0
votes

This is a follow-up question to Crop and resize image around a custom focus point where I got helpful answers on what command parts to use for imagemagick in order to achieve what I want: Scale an image down, but instead of using a predefined gravitiy (like "Center" or "North"), I want to use a custom point of focus on the image that should be the new center of the scaled image to ensure the "important point" of the image is always visible when cropping/resizing it.

This works quite well, except for the actual size calculation. I guess something is wrong in my math, because the resulting output is missing some parts that could be visible if the image is being scaled down more than I calculate. I can't find the mistake I'm doing, maybe someone can give me a hint into the right direction.

What I am currently doing:

# Example image from https://unsplash.com/photos/p-I9wV811qk
source_image = 'input.jpg'

source_image_size = [3008, 2000]
target_image_size = [295, 195]

# Focus point position, in percentages.
focus_point = {
  x: 46.7087766,
  y: 24.2
}

# Calculate the focus point in px on the source image.
source_focus_point = [
  source_image_size[0] / 100.0 * focus_point[:x],
  source_image_size[1] / 100.0 * focus_point[:y]
]

# Calculate the distances to the focus point in percentage,
# from all four image sides, and use the smalles value.
smallest_percentage_distance = [
  focus_point[:x],
  (100.0 - focus_point[:x]),
  focus_point[:y],
  (100.0 - focus_point[:y])
].min

# Scale image to reach smallest percentage distance.
scale_factor = smallest_percentage_distance / 100.0

# Calculate the focus point on the target image for the transformation,
# which will be on the center of the new image.
target_focus_point = [
  target_image_size[0].to_f / 2.0,
  target_image_size[1].to_f / 2.0
]

# Define how many degrees the image should be rotated.
rotation_degrees = 0

command = [
  'convert',
  source_image,
  "-set option:distort:viewport #{target_image_size.join('x')}",
  "-distort SRT '#{source_focus_point.join(',')} #{scale_factor} #{rotation_degrees} #{target_focus_point.join(',')}'",
  'output2.jpg'
].join ' '

The resulting command is:

convert input.jpg -set option:distort:viewport 295x195 -distort SRT '1405.000000128,484.0 0.242 0 147.5,97.5' output.jpg

In my understanding, imagemagick's distort SRT's second argument (here, scale_factor) defines how much, in %, the image has to be scaled down. My idea was to calculate the distances of all sides to the focus point, take the smallest distance, convert it to a suitable argument, and use that one for defining the scaling amount.

The goal is to scale the image as much as possible to ensure that the output image does not crop off too much in order to show as much as possible of the original image. My output right now cuts off too much.

I tried to visualize it for a better understanding: enter image description here

The original image can be found here: https://unsplash.com/photos/p-I9wV811qk

What am I doing wrong? How do I calculate the value for scaling down the image correctly to have as much of the picture visible as possible?


Edit 1
At the end, multiple variations of the image will be generated, having different aspect ratios. Next to the given 295x195, another one will be 1440x560; Especially in this one, a simple "crop with gravity" will most likely crop out the middle or upper part of the image, thus the focus point to ensure the vital part is always visible.


Edit 2 to the answer from fmw42
The code works so far that it does resizing and cropping without using SRT. Unfortunately, the result is further away compared to the result I'm currently having (but maybe I did something wrong when adapting the script).

What I did was

  • using the image from my question
  • replacing px and py with the focus point values from my question
  • keeping the scale calculation but using wd=295 and ht=195 as this is the desired target crop

The resulting output looks like this: enter image description here

2
You do not need to go to all that trouble. Just scale the whole image by whatever percent you want. It will all be there. Then crop the region you want by computing the scaled down point relative to the center. And crop with -gravity center.fmw42
I don't know the percentage of scaling it down to have everything visible, that's basically the main question; How do I calculate this based on the information I have (original image size, target image size, focus point x,y coordinates). If I just scale it down and then crop with center gravity, as far as I understand, the crop starts from the center of the image, but that would not respect the focus point at all, which could be on top or on the bottom.pdu
If you use -resize WxH (in pixels or percent), the whole image gets resized. So everything is visible and nothing is cut off. Just compute the offset of the point from the center in your input and scale that to compute the offset in the scaled down version. Use -gravity center -crop for any size you want.fmw42
Sounds like a good plan. Would you be able to provide an example? I played around with resize but struggle a bit with understanding how to exactly implement your idea.pdu
Edited my question ("Edit 1" at the bottom of the question) as I feel like the use case may be important.pdu

2 Answers

1
votes

Here is how I would do that in Imagemagick (Unix syntax)

Input:

enter image description here

# get image dimensions and aspect ratio
WxH=`convert barn.jpg -format "%wx%h" info:`
ww=`echo "$WxH" | cut -dx -f1`
hh=`echo "$WxH" | cut -dx -f2`
aspect=`convert xc: -format "%[fx:$ww/$hh]" info:`

# define centering point
px=200
py=200

# compute distances to each side of the image
dleft=$px
dright=$((ww-px-1))
dtop=$py
dbottom=$((hh-py-1))

# find the minimum distance and to which side
dminx=`convert xc: -format "%[fx:round(min($dleft,$dright))]" info:`
dminy=`convert xc: -format "%[fx:round(min($dtop,$dbottom))]" info:`
dmin=`convert xc: -format "%[fx:round(min($dminx,$dminy))]" info:`

# compute crop width, height and scale percent
if [ $dmin -eq $dminx ]; then
wd=$((2*dminx))
ht=`convert xc: -format "%[fx:round($wd/$aspect)]" info:`
scale=`convert xc: -format "%[fx:100*$ww/$wd]" info:`
else
ht=$((2*dminy))
wd=`convert xc: -format "%[fx:round($ht*$aspect)]" info:`
scale=`convert xc: -format "%[fx:100*$hh/$ht]" info:`
fi

# compute x and y offsets to the top left corner of crop region
xoff=`convert xc: -format "%[fx:round($px-$wd/2)]" info:`
yoff=`convert xc: -format "%[fx:round($py-$ht/2)]" info:`

# crop and resize the image to original dimensions
convert barn.jpg -crop ${wd}x${ht}+${xoff}+${yoff} +repage -resize $scale% barn_result.jpg


Result:

enter image description here

1
votes

If your center point is too close to a side of the image, it may not support either of those sizes or aspect ratios. What do you want done in those cases? Should the image be smaller than your desired sizes or padded with black or transparent? Or should the desired size be maintained and the desired center not be in the center?

Here is an ImageMagick solution, assuming I now understand what you want, that will crop the desired sizes and aspects. If the center is too close to one side, the output size will still be as desired, but the desired point will not be in the center. I have marked the desired point in relative to the coordinates in the input with a small red circle.

Once again, Unix syntax.

Input:

enter image description here

Desired point at 200,200:

cd
cd desktop
infile="barn.jpg"
inname=`convert "$infile" -format "%t" info:`
# get image dimensions and aspect ratio
WxH=`convert "$infile" -format "%wx%h" info:`
ww=`echo "$WxH" | cut -dx -f1`
hh=`echo "$WxH" | cut -dx -f2`

# define centering point
px=50
py=50

# define crop size
wd=295
ht=195

# check if enough space on each side
left=`convert xc: -format "%[fx:round($px-$wd/2)]" info:`
left=`convert xc: -format "%[fx:$left<0?0:$left]" info:`
right=`convert xc: -format "%[fx:$left+$wd-1]" info:`
right=`convert xc: -format "%[fx:$right>$ww-1?$ww-1:$right]" info:`
top=`convert xc: -format "%[fx:round($py-$ht/2)]" info:`
top=`convert xc: -format "%[fx:$top<0?0:$top]" info:`
bottom=`convert xc: -format "%[fx:$top+$ht-1]" info:`
bottom=`convert xc: -format "%[fx:$bottom>$hh-1?$hh-1:$bottom]" info:`

# compute crop values
xoff=$left
yoff=$top
width=`convert xc: -format "%[fx:round($right-$left+1)]" info:`
height=`convert xc: -format "%[fx:round($bottom-$top+1)]" info:`
echo "$left; $right; $top; $bottom;"
echo "$xoff; $yoff; $width; $height"

# crop and resize the image to original dimensions
convert "$infile" -fill red -draw "translate $px,$py circle 0,0 0,2" -alpha off \
-crop ${width}x${height}+${xoff}+${yoff} +repage ${inname}_${px}_${py}.jpg


enter image description here

Desired point at 50,50:

enter image description here