From 6b46e0ba5c725ffe2db75c2e660d4f4c9c4461aa Mon Sep 17 00:00:00 2001 From: Troy Sornson Date: Wed, 12 Jul 2023 20:37:44 -0600 Subject: [PATCH] Implement image rotation, default value for color channels, fix bug with alpha --- spec/cr-image/operation/rotate_spec.cr | 46 ++++++++++++++++++++++++++ src/cr-image/channel_type.cr | 4 +++ src/cr-image/image.cr | 9 ++--- src/cr-image/map.cr | 10 +++--- src/cr-image/operation/pad.cr | 2 +- src/cr-image/operation/rotate.cr | 43 ++++++++++++++++++++++++ src/cr-image/rgba_image.cr | 5 +++ src/lib-formats/jpeg.cr | 8 ++--- 8 files changed, 113 insertions(+), 14 deletions(-) create mode 100644 spec/cr-image/operation/rotate_spec.cr create mode 100644 src/cr-image/operation/rotate.cr diff --git a/spec/cr-image/operation/rotate_spec.cr b/spec/cr-image/operation/rotate_spec.cr new file mode 100644 index 0000000..18004e0 --- /dev/null +++ b/spec/cr-image/operation/rotate_spec.cr @@ -0,0 +1,46 @@ +require "../../spec_helper" + +Spectator.describe CrImage::Operation::Rotate, :focus do + include SpecHelper + + specs_for_operator(rotate(45), + gray_hash: "b23e83666e8046bd28396b3c5d7e4c7874671cfd", + rgba_hash: "e21797526425cd7e9473ca479f45bbdb8e049d1e" + ) + + specs_for_operator(rotate(90), + gray_hash: "124795c3ed7e003750d024f610626c3b22b26206", + rgba_hash: "fd20c71bb52ff5a148dfcea9f4fe49ab92c80804" + ) + + specs_for_operator(rotate(-30), + gray_hash: "a14c40f3c480330e4ec8aa9d8441d3e08d6e3864", + rgba_hash: "edafe8f2472918dcdad5e72187665fcc32e14851" + ) + + specs_for_operator(rotate(45, center_x: 100, center_y: 200), + gray_hash: "89f91776edc2c8814d235c29d0a50eaa3f015753", + rgba_hash: "fb4a52471956cd6a48aee2f4470477a4b295587c" + ) + + specs_for_operator(rotate(45, center_x: 375, center_y: 250, radius: 20), + gray_hash: "171f3aa256a46a3aeec94cddec477ed01fdb658e", + rgba_hash: "ba9d444ed5f4567830d6037990231aae0f3431ca" + ) + + # specs_for_operator(crop(0, 0, 750, 500), + # gray_hash: "62d6101d60ee8da38d1b9d8e809091099cec5994", + # rgba_hash: "d764459f778b4839972367f78197bf9a96cd11fd" + # ) + + let(image) { gray_moon_ppm } + + it "rotates" do + image.to_rgba + .draw_square(0, 0, image.width - 1, image.height - 1, CrImage::Color.random) + .draw_circle(375, 250, 5, CrImage::Color.of("#ff0000"), fill: true) + .save("original.jpg") + .rotate(-45, center_x: 375, center_y: 250, radius: 20, pad: true) + .save("rotated.jpg") + end +end diff --git a/src/cr-image/channel_type.cr b/src/cr-image/channel_type.cr index 2468af5..ebe92f0 100644 --- a/src/cr-image/channel_type.cr +++ b/src/cr-image/channel_type.cr @@ -7,4 +7,8 @@ enum CrImage::ChannelType Blue Gray Alpha + + def default : UInt8 + alpha? ? 255u8 : 0u8 + end end diff --git a/src/cr-image/image.cr b/src/cr-image/image.cr index f806259..bbb1cb1 100644 --- a/src/cr-image/image.cr +++ b/src/cr-image/image.cr @@ -17,13 +17,14 @@ abstract class CrImage::Image include Operation::Brightness include Operation::ChannelSwap include Operation::Contrast - include Operation::GaussianBlur - include Operation::HorizontalBlur - include Operation::VerticalBlur include Operation::Crop - include Operation::Pad include Operation::Draw + include Operation::GaussianBlur include Operation::HistogramEqualize + include Operation::HorizontalBlur + include Operation::Rotate + include Operation::Pad + include Operation::VerticalBlur include Format::Save extend Format::Open diff --git a/src/cr-image/map.cr b/src/cr-image/map.cr index 7c9a11a..a86f77f 100644 --- a/src/cr-image/map.cr +++ b/src/cr-image/map.cr @@ -265,14 +265,14 @@ module CrImage @raw.dup end - def pad(all : Int32 = 0, *, top : Int32 = 0, bottom : Int32 = 0, left : Int32 = 0, right : Int32 = 0, pad_type : EdgePolicy = EdgePolicy::Black) : self + def pad(all : Int32 = 0, *, top : Int32 = 0, bottom : Int32 = 0, left : Int32 = 0, right : Int32 = 0, pad_type : EdgePolicy = EdgePolicy::Black, pad_black_value : T = T.zero) : self top = top > 0 ? top : all bottom = bottom > 0 ? bottom : all left = left > 0 ? left : all right = right > 0 ? right : all new_width = left + width + right - new_raw = initial_raw_pad(pad_type, new_width, top, bottom, left, right) + new_raw = initial_raw_pad(pad_type, new_width, top, bottom, left, right, pad_black_value) # Now copy the original values into the new raw array at the correct locations height.times do |y| @@ -299,12 +299,12 @@ module CrImage self.class.new(new_width, new_raw) end - private def initial_raw_pad(pad_type, new_width, top, bottom, left, right) : Array(T) + private def initial_raw_pad(pad_type, new_width, top, bottom, left, right, pad_black_value) : Array(T) case pad_type - in EdgePolicy::Black then Array(T).new((top + height + bottom) * new_width) { T.zero } + in EdgePolicy::Black then Array(T).new((top + height + bottom) * new_width) { pad_black_value } in EdgePolicy::Repeat then Array(T).new((top + height + bottom) * new_width) do |i| current_y = i // new_width - next T.zero if (current_y) < top || current_y >= (top + height) + next pad_black_value if (current_y) < top || current_y >= (top + height) adjusted_y = (current_y) - top adjusted_x = i % new_width diff --git a/src/cr-image/operation/pad.cr b/src/cr-image/operation/pad.cr index b008975..75bbf65 100644 --- a/src/cr-image/operation/pad.cr +++ b/src/cr-image/operation/pad.cr @@ -18,7 +18,7 @@ module CrImage::Operation::Pad def pad!(all : Int32 = 0, *, top : Int32 = 0, bottom : Int32 = 0, left : Int32 = 0, right : Int32 = 0, pad_type : EdgePolicy = EdgePolicy::Black) : self orig_width = width each_channel do |channel, channel_type| - padded = UInt8Map.new(orig_width, channel).pad(all, top: top, bottom: bottom, left: left, right: right, pad_type: pad_type) + padded = UInt8Map.new(orig_width, channel).pad(all, top: top, bottom: bottom, left: left, right: right, pad_type: pad_type, pad_black_value: channel_type.default) self[channel_type] = padded.raw @width = padded.width @height = padded.height diff --git a/src/cr-image/operation/rotate.cr b/src/cr-image/operation/rotate.cr new file mode 100644 index 0000000..5fe6882 --- /dev/null +++ b/src/cr-image/operation/rotate.cr @@ -0,0 +1,43 @@ +module CrImage::Operation::Rotate + def rotate(degrees : Float64, *, center_x : Int32 = width // 2, center_y : Int32 = height // 2, radius : Int32 = -1, pad : Bool = false) : self + clone.rotate!(degrees, center_x: center_x, center_y: center_y, radius: radius, pad: pad) + end + + def rotate!(degrees : Float64, *, center_x : Int32 = width // 2, center_y : Int32 = height // 2, radius : Int32 = -1, pad : Bool = false) : self + # TODO: use pad + + # Rotate backwards, so that we can "look back" from the output pixel location into the input pixel location + radians = -Math::PI * degrees / 180 + sin = Math.sin(radians) + cos = Math.cos(radians) + radius_sq = radius * radius + + new_size = size + out_center_x = center_x + out_center_y = center_y + each_channel do |channel, channel_type| + new_x = -1 + new_y = -1 + new_channel = Array.new(new_size) do |i| + new_x += 1 + new_x %= width + new_y += 1 if new_x == 0 + + if radius >= 0 + next channel[i] if ((new_x - center_x) ** 2 + (new_y - center_y) ** 2) > radius_sq + end + + orig_x = (cos * (new_x - center_x) - sin * (new_y - center_y) + out_center_x).round.to_i + orig_y = (sin * (new_x - center_x) + cos * (new_y - center_y) + out_center_y).round.to_i + + next channel_type.default if orig_x < 0 || orig_x >= width || orig_y < 0 || orig_y >= height + + channel.unsafe_fetch(orig_y * width + orig_x) + end + + self[channel_type] = new_channel + end + + self + end +end diff --git a/src/cr-image/rgba_image.cr b/src/cr-image/rgba_image.cr index 4639ce9..2ad5944 100644 --- a/src/cr-image/rgba_image.cr +++ b/src/cr-image/rgba_image.cr @@ -89,6 +89,11 @@ class CrImage::RGBAImage < CrImage::Image ) end + # Returns self + def to_rgba : RGBAImage + self + end + # Return the number of pixels in this image def size : Int32 @width * @height diff --git a/src/lib-formats/jpeg.cr b/src/lib-formats/jpeg.cr index 34a73c6..4f74def 100644 --- a/src/lib-formats/jpeg.cr +++ b/src/lib-formats/jpeg.cr @@ -45,10 +45,10 @@ module CrImage::Format::JPEG ) check_jpeg handle, LibJPEGTurbo.destroy(handle) - red = Array.new(width * height) { 0u8 } - green = Array.new(width * height) { 0u8 } - blue = Array.new(width * height) { 0u8 } - alpha = Array.new(width * height) { 255u8 } + red = Array.new(width * height) { ChannelType::Red.default } + green = Array.new(width * height) { ChannelType::Green.default } + blue = Array.new(width * height) { ChannelType::Blue.default } + alpha = Array.new(width * height) { ChannelType::Alpha.default } (width * height).times do |index| red.unsafe_put(index, buffer[index * 3])