Imagemagick rails image composition adding unwanted white background - ruby-on-rails

I have the following code:
begin
big_image = Magick::ImageList.new
#this is an image containing first row of images
first_row = Magick::ImageList.new
#adding images to the first row (Image.read returns an Array, this is why .first is needed)
first_row.push(Magick::Image.read(Rails.root.join("app","assets","images","logo.png")).first)
if #model.avatar.exists?
image = Magick::Image.read(#model.avatar.path).first
image = image.resize_to_fit("450", "401")
first_row.push(image)
end
#adding first row to big image and specify that we want images in first row to be appended in a single image on the same row - argument false on append does that
big_image.push (first_row.append(false))
fileName = #model.id.to_s + ".png"
big_image.append(true).write(Rails.root.join("app","assets","images","shared_logo",fileName))
rescue => e
puts "Errors! -- #{e.inspect}"
end
The codes puts two image on the same row. The images are png. The problem is that the second image has an height less than the first one. Image magick fill the remaining part with an unwanted white background. I want to keep the transparency on the combined image.

You could do something like:
manipulate! do |img|
img.combine_options do |c|
c.background "transparent"
c.gravity "center"
c.extent "450x401"
end
end
This is for RMagick if I'm not mistaken, the names might be slightly different (although I think combine options is using mongrifies methods?).

Related

Removing a tick mark made using Pen in a scanned pdf

I have a PDF file with Multiple choise questions . The answered are marked . I would like to remove the "tick"portion of it without majorly altering the sheet . I would like to automate it for the entire PDF .In the images below , would like to remove the "tick" mark alone . My input file is a PDF . For sake of representation have give a image .
Note . I use GNU/Linux
[
The main difference that I can see between the checkmark and the rest of the image is the thickness of the black line. Thus we can replace the thicker black line with a sample of the background of your image
Here is a way to do this using Python/OpenCV/Numpy:
import cv2
import numpy as np
#load the image
img = cv2.imread("pen_mark.png")
#Threshold the image to a 2-channel black and white image with the text as white and the background as black.
black_and_white = (img < 128).astype(np.uint8) * 255
#erode the thin lines, leaving only the thicker pen-drawn checkmark.
checkmark = cv2.morphologyEx(black_and_white, cv2.MORPH_ERODE, np.ones((5,5)))
#dilate the remaining checkmark back to slightly larger than its original size
checkmark = cv2.morphologyEx(checkmark, cv2.MORPH_DILATE, np.ones((7,7)))
#switch the left and right side of the original image in order to get a sample of the
#original speckled background overlaying the position of the checkmark.
rolled_img = np.roll(img, shift=img.shape[1]//2)
#replace the pixels of the original checkmark with the background
img[checkmark.nonzero()] = rolled_img[checkmark.nonzero()]
#save the new image
cv2.imwrite("result.png", img)
Note that this probably won't be a reliable solution for all of your PDFs. I assume the thickness of the pen mark will change. There will probably also be cases where the pen mark overlaps other lines and you'll end up with blank spots. Another problem is the difficulty in setting the background so that it blends nicely with its surroundings.

Parallel loading images in Julia

I am trying to create mosaic images of simulation data where each tile is in .jpg format and has a fixed number of pixels. I combine hundreds of these into a larger image for easier parameter analysis. So far I was able to parallelize getting the tiles to create a larger image with the following code:
using Distributed
addprocs(8)
# add #everywhere macro before every function and variable so it magically works
#everywhere big_image = zeros(RGB, 300, 1000); #each tile has 100x100 pixels for simplicity
function createBigFrame(big_image)
#sync for row = 1:10 # #sync waits until all the images are fetched and then continue with plotting
for col = 1:3
i = #range for x
j = #range for y (or vice versa)
image_path = #get the path
Threads.#spawn big_image[i, j] = load(image_path);
end
 end
plot(...) # add axes and ticks to the image
savefig(...) # save the figure on the disk
end
Although I used this on small data, it gave me a 20% increase in performance. Higher performance will be seen with larger data since there are more tile images to parallel. However, it has been told me that this is not the proper way of parallelizing things. I am very curious to know the right way to load images in parallel (not concurrently since the load() function is not thread-safe) and improve the code and performance further. I am very grateful for your help.
EDIT: The following code is supposed to be a minimal working example, but since you don't have the files to load(), the situation is a little different.
using Distributed
addprocs(4) #do not re-run
#everywhere using Colors, Images
#everywhere img = Images.zeros(RGB, 300, 1000);
function createBigFrame(image)
#sync for row = 1:10
for col = 1:3
j = 100*(col-1)+1:100*col;
i = 100*(row-1)+1:100*row;
Threads.#spawn image[j, i] .= RGB(rand(3)...)
end
 end
return image
end
createBigFrame(img)
I managed to run my code as the second suggestion of the selected answer in here. However, I'm not sure I fully understand how the code works under the hood.
I'll put this example picture here so we are on the same page:
There are 3 rows and 10 columns of tiles in this image.
Now, in my first attempt, I used the following way to get the images to row 1 in parallel.
arr = SharedArray{Float32,3}(ones(3, 512*3, 512*10))
#sync #distributed for row = 1:3
for col = 1:10
row_index = #index range for the height of the image
col_index = #index range for the width of the image
arr[:, row_index, col_index] = channelview(testimage("lake_color"))
end
end
If my understanding is correct, #sync is used to wait for each row iteration to complete while workers fetch images in that row in parallel. I thought if this is the case, there should be a waste of time between rows waiting for the previous row to complete.
So I flattened the 2D fetching loop to 1D in order to have more space to move and fetch the images freely:
#sync #distributed for s = 1:30
row_index = #index range for the height of the image
col_index = #index range for the width of the image
arr[:, row_index, col_index] = channelview(testimage("lake_color"))
end
But I was wrong? The second code came out 25% slower than the first one (150 seconds to 190 seconds), so I wonder what is going on here?

Compose lots of images at once in imagemagick Ruby

I have the following code which takes a PDF file and composes it into a single jpg image which has a horizontal black line between each PDF page image, stacking the PDF pages.
image = MiniMagick::Image.open(pdf_file)
# create a new blank file which we will use to build a composite image
# containing all of our pages
MiniMagick::Tool::Convert.new do |i|
i.size "#{image.width}x#{image.layers.size * image.height}"
i.stroke "black"
image.layers.count.times.each do |ilc|
next if ilc.zero?
top = ilc * (image.height + 1)
i.draw "line 0,#{top}, #{image.width},#{top}"
end
i.xc "white"
i << image_file_name
end
composite_image = MiniMagick::Image.open(image_file_name)
# For each pdf page, add it to our composite image. We add one so that we
# don't put the image over the 1px black line that was added to separate
# pages.
image.layers.count.times do |i|
composite_image = composite_image.composite(image.layers[i]) do |c|
c.compose "Over" # OverCompositeOp
c.geometry "+0+#{i * (image.height + 1)}"
end
end
composite_image.format(format)
composite_image.quality(85)
composite_image.write(image_file_name)
It works perfectly, except a 20 page PDF file takes three minutes. I'm looking for a better way to do this. I suspect one of these two options will work:
Compose all of the PDF page images at once, although I haven't figured out how to do that.
Use vips, thanks to its pipeline implementation.
I would rather stay with imagemagick, but I am open to either way. I'm looking for pointers how to achieve what I am looking for.
I had a stab at a ruby-vips version:
require 'vips'
# n: is the number of pages to load, -1 means all pages in tall, thin image
image = Vips::Image.pdfload ARGV[0], n: -1
# we can get the number of pages and the height of each page from the metadata
n_pages = image.get 'pdf-n_pages'
page_height = image.get 'page-height'
# loop down the image cutting it into an array of separate pages
pages = (0 ... n_pages).map do |page_number|
image.crop(0, page_number * page_height, image.width, page_height)
end
# make a 50-pixel-high black strip to separate each page
strip = Vips::Image.black image.width, 50
# and join the pages again
image = pages.inject do |acc, page|
acc.join(strip, 'vertical').join(page, 'vertical')
end
image.write_to_file ARGV[1]
On this desktop with this 58 page PDF I see:
$ /usr/bin/time -f %M:%e ruby ./pages.rb nipguide.pdf x.jpg
152984:1.08
$ vipsheader x.jpg
x.jpg: 595x50737 uchar, 3 bands, srgb, jpegload
So it makes a 50,000 pixel high jpg in about 1.1 seconds and needs a peak of 150 mb of memory.
I tried fmw42's clever imagemagick line:
$ /usr/bin/time -f %M:%e convert nipguide.pdf -background black -gravity south -splice 0x50 -append x.jpg
492244:5.16
so 500 mb of memory and 5.2s. It makes an image almost exactly the same size.
The speed difference is mostly the PDF rendering library, of course: IM shells out to ghostscript, whereas ruby-vips calls poppler or PDFium directly. libvips is able to stream this program, so during evaluation it never has more than one page in memory at once.
JPG has a limit of 65535 pixels in any axis, so you won't be able to get much larger than this. For shorter documents, you could add dpi: 300 to the PDF load to get more detail. The default is 72 dpi.
You should get nice text quality without having to render at high resolution. For example, for the PDF linked above, if I run:
$ vips pdfload nipguide.pdf x.png --page 12
To render page 12 at the default 72 dpi, I get:
I am not sure this is what you want, but it seems to me from your description, you want to append the images.
I created a 3-page PDF from 3 jpg images just for testing. I then add black border (in this case 10 pixels to show it better) at the bottom of each page and then append all the pages.
This was done with Imagemagick 6.9.10.12 Q16, but I suspect Python Wand or minimagick has similar functionality.
convert test.pdf -background black -gravity south -splice 0x10 -append test.jpg
If necessary, you could chop off the black line at the bottom of the last page after the append using -chop 0x10.

Unable to retain transparency using ImageMagick's composite

I have two images (both png) with transparency. I am using the MiniMagick gem to crop two copies of a single image into two other images. I'm then wanting to compose one of these images on top of the other, retaining the transparency all the way down.
Using the following code, it is respecting the transparency of image2, but once it is placed on top of image1 (which is what I'm after), the transparency of image1 is changed to black! I need to retain the transparency, but I'm really not sure how to properly use the alpha transparency stuff, if that is even the proper tool here.
image = MiniMagick::Image.open("skin.png")
image1 = MiniMagick::Image.open(image.path)
image2 = MiniMagick::Image.open(image.path)
# Crop and scale image1
MiniMagick::Tool::Mogrify.new do |m|
m.crop '8x8+8+8'
m.scale '144x144'
m.background 'transparent'
m.extent '160x160-8-8'
m << image1.path
end
# Crop and scale image2
MiniMagick::Tool::Mogrify.new do |m|
m.crop '8x8+40+8'
m.scale '160x160'
m << image2.path
end
result = image1.composite(image2) do |c|
c.compose 'Over'
c.alpha 'On'
end
result.write "public/skins/#{profile}.png"
send_file "public/skins/#{profile}.png"
Thanks.
MiniMagick's composite is processed as jpg with initial value.
http://www.rubydoc.info/github/probablycorey/mini_magick/MiniMagick/Image#composite-instance_method
The code below is working for me.
result = image1.composite(image2, 'png') do |c|
c.channel "A"
c.alpha 'Activate'
c.compose 'Over'
end

Take center part of image while resizing

I have a problem with resizing image with Carrierwave-MiniMagick-ImageMagick.
I wrote custom method of resizing, cause I need to merge 2 images together and make some processing on them, so standard process methods from MiniMagick are not enough. The problem is with resize method. I need to take center part of the image, but it returns me the top part.
def merge
manipulate! do |img|
img.resize '180x140^' # problem is here
...
img
end
end
Thanks for any help!
I would approach this as follows:
Resize image to a 180x180 square
Remove (180-140)/2 from the top
Remove (180-140)/2 from the bottom
Something like this should do it:
def merge
manipulate! do |img|
img.resize '180x180' # resize to 180px square
img.shave '0x20' # Removes 20px from top and bottom edges
img # Returned image should be 180x140, cropped from the centre
end
end
Of course, this assumes your input image is always a square. If it wasn't square and you've got your heart set on the 180x140 ratio, you could do something like this:
def merge
manipulate! do |img|
if img[:width] <= img[:height]
# Image is tall ...
img.resize '180' # resize to 180px wide
pixels_to_remove = ((img[:height] - 140)/2).round # calculate amount to remove
img.shave "0x#{pixels_to_remove}" # shave off the top and bottom
else
# Image is wide
img.resize 'x140' # resize to 140px high
pixels_to_remove = ((img[:width] - 180)/2).round # calculate amount to remove
img.shave "#{pixels_to_remove}x0" # shave off the sides
end
img # Returned image should be 180x140, cropped from the centre
end
end
This is what resize_to_fill does:
Resize the image to fit within the specified dimensions while retaining the aspect ratio of the original image. If necessary, crop the image in the larger dimension.
Example:
image = ImageList.new(Rails.root.join('app/assets/images/image.jpg'))
thumb = image.resize_to_fill(1200, 630)
thumb.write('thumb.jpg')
The method takes a third argument which is gravity, but it's CenterGravity by default.
You should use crop instead of resize.
Take a look at crop ImageMagick description of crop command here:
http://www.imagemagick.org/script/command-line-options.php#crop
MiniMagick just a wrapper around ImageMagick, so all arguments are the same.

Resources