math/algorithm Fit image to screen retain aspect ratio - algorithm

I need help with math / algorithm to take an image of known size and fit to one of two screen dimensions:
720 x 480 or 1280 x 1024.
The image dimensions are coming from an XML file, however those dimensions are the web dimensions, I also get a selection of images from the XML that may be of higher and lower resolution than the web dimensions.
What I want is to use the aspect ration of the web dimensions to display the higher resolution image, if available, on an HD (1280x720) screen, or, if the user is on an SD screen (720x480) display the image on that screen.
Other things that would be useful for this, but lower priority, would be, if I know the resolution of the image is smaller in both dimensions than an SD screen (in this case, all I know is the web dimension, and the horizontal dimension of the image file), to display it as actual size on that screen.

Generic as can be:
Image data: (wi, hi) and define ri = wi / hi
Screen resolution: (ws, hs) and define rs = ws / hs
Scaled image dimensions:
rs > ri ? (wi * hs/hi, hs) : (ws, hi * ws/wi)
So for example:
20
|------------------|
10
|---------|
-------------------- --- ---
| | | | 7 |
| | | | | 10
|---------- | --- |
| | |
-------------------- ---
ws = 20
hs = 10
wi = 10
hi = 7
20/10 > 10/7 ==> (wi * hs/hi, hs) = (10 * 10/7, 10) = (100/7, 10) ~ (14.3, 10)
Which as you can see clearly scales to the screen size, because the height is that of the screen but clearly keeps aspect ratio since 14.3/10 ~ 10/7
UPDATE
Center the image as follows:
call (wnew, hnew) the new dimensions.
top = (hs - hnew)/2
left = (ws - wnew)/2

I understand the accepted answer and it works, but I've always found the following method to be simpler and succinct for "best fit":
// prep
let maxWidth = 190,
maxHeight = 150;
let imgWidth = img.width,
imgHeight = img.height;
// calc
let widthRatio = maxWidth / imgWidth,
heightRatio = maxHeight / imgHeight;
let bestRatio = Math.min(widthRatio, heightRatio);
// output
let newWidth = imgWidth * bestRatio,
newHeight = imgHeight * bestRatio;
... which of course can be distilled down to:
const maxWidth = 190, maxHeight = 150;
const bestRatio = Math.min(maxWidth / img.width, maxHeight / img.height);
img.width *= bestRatio;
img.height *= bestRatio;

Here it is in straightforward C.
You want to scale both coordinates by the returned scale factor.
/* For a rectangle inside a screen, get the scale factor that permits the rectangle
to be scaled without stretching or squashing. */
float
aspect_correct_scale_for_rect(const float screen[2], const float rect[2])
{
float screenAspect = screen[0] / screen[1];
float rectAspect = rect[0] / rect[1];
float scaleFactor;
if (screenAspect > rectAspect)
scaleFactor = screen[1] / rect[1];
else
scaleFactor = screen[0] / rect[0];
return scaleFactor;
}

Aspect ratio correction with letterboxing or fit-to-screen
I wrote up a method recently to handle this exact problem in iOS. I'm using the Eigen matrix library to do scaling, but the the principle (scaling factor) is the same without matrices.
Eigen::Matrix4x4f aspectRatioCorrection(bool fillScreen, const Eigen::Vector2f &screenSize, const Eigen::Vector2f &imageSize)
{
Eigen::Matrix4x4f scalingMatrix(Eigen::Matrix4x4f::Identity());
float screenWidth = screenSize.x();
float screenHeight = screenSize.y();
float screenAspectRatio = screenWidth / screenHeight;
float imageWidth = imageSize.x();
float imageHeight = imageSize.y();
float imageAspectRatio = imageWidth / imageHeight;
float scalingFactor;
if (fillScreen) {
if (screenAspectRatio > imageAspectRatio) {
scalingFactor = screenWidth / imageWidth;
} else {
scalingFactor = screenHeight / imageHeight;
}
} else {
if (screenAspectRatio > imageAspectRatio) {
scalingFactor = screenHeight / imageHeight;
} else {
scalingFactor = screenWidth / imageWidth;
}
}
scalingMatrix(0, 0) = scalingFactor;
scalingMatrix(1, 1) = scalingFactor;
return scalingMatrix;
}

Related

Three JS - Scaling texture to fit a (any size) Plane perfectly

In essence, I want to replicate the behaviour of how the CSS, background-size: cover works.
Looking here you can see the image is being scaled keeping its aspect ratio, but it's not really working correctly, as the image does not fill the Plane, leaving margins either side - https://next.plnkr.co/edit/8650f9Ji6qWffTqE?preview
Code snippet (Lines 170 - 175) -
var geometryAspectRatio = 5/3;
var imageAspectRatio = 3264/2448;
textTile.wrapT = THREE.RepeatWrapping;
textTile.repeat.x = geometryAspectRatio / imageAspectRatio;
textTile.offset.x = 0.5 * ( 1 - textTile.repeat.x );
What I want to happen is for it so scale-up and then reposition its self in the centre (much how cover works).
var repeatX, repeatY;
repeatX = w * this.textureHeight / (h * this.textureWidth);
if (repeatX > 1) {
//fill the width and adjust the height accordingly
repeatX = 1;
repeatY = h * this.textureWidth / (w * this.textureHeight);
mat.map.repeat.set(repeatX, repeatY);
mat.map.offset.y = (repeatY - 1) / 2 * -1;
} else {
//fill the height and adjust the width accordingly
repeatX = w * this.textureHeight / (h * this.textureWidth);
repeatY = 1;
mat.map.repeat.set(repeatX, repeatY);
mat.map.offset.x = (repeatX - 1) / 2 * -1;
}
Updated https://next.plnkr.co/edit/LUk37xLG2yvv6hgg?preview
For anyone confused by this as I was, the missing piece for me is that .repeat.x and .repeat.y properties of any texture can be values less than one, and scales up the image when is under 1 as the inverse of the scale. Think about it, when it's scale 2, in a way it repeats .5 times because you only see half of the image.
So...
Something not supported by textures in THREE.js and common in some libraries, would be
.scaleX = 2; (not supported in THREE.js textures as of v1.30.1)
And the THREE.js texture equivalent would be
texture.repeat.x = .5;
To convert scale to "repeat", simply do the inverse of the scale
var desiredScaleX = 3;
var desiredRepeatX = 1 / desiredScaleX;
The repeat for scale 3 comes out to (1/3) = .3333; In other words a 3x image would be cropped and only show 1/3 of the image, so it repeats .3333 times.
As for scaling to fit to cover, generally choosing the larger scale of the two will do the trick, something like:
var fitScaleX = targetWidth / actualWidth;
var fitScaleY = targetHeight / actualHeight;
var fitCoverScale = Math.max(fitScaleX,fitScaleY);
var repeatX = 1 / fitCoverScale;
var repeatY = 1 / fitCoverScale;

Image resizing algorithm

I want to write a function to downsize an image to fit specified bounds. For example i want to resize a 2000x2333 image to fit into 1280x800. The aspect ratio must be maintained. I've come up with the following algorithm:
NSSize mysize = [self pixelSize]; // just to get the size of the original image
int neww, newh = 0;
float thumbratio = width / height; // width and height are maximum thumbnail's bounds
float imgratio = mysize.width / mysize.height;
if (imgratio > thumbratio)
{
float scale = mysize.width / width;
newh = round(mysize.height / scale);
neww = width;
}
else
{
float scale = mysize.height / height;
neww = round(mysize.width / scale);
newh = height;
}
And it seemed to work. Well ... seemed. But then i tried to resize a 1280x1024 image to a 1280x800 bounds and it gave me a result of 1280x1024 (which obviously doesn't fit in 1280x800).
Does anybody have any ideas how this algorithm should work?
The way I usually do this is to look at the ratio between the original width and the new width and the ratio between the original height and the new height.
After this shrink the image by the biggest ratio. For example, if you wanted to resize an 800x600 image into a 400x400 image the width ratio would be 2, and the height ratio would be 1.5. Shrinking the image by a ratio of 2 gives a 400x300 image.
NSSize mysize = [self pixelSize]; // just to get the size of the original image
int neww, newh = 0;
float rw = mysize.width / width; // width and height are maximum thumbnail's bounds
float rh = mysize.height / height;
if (rw > rh)
{
newh = round(mysize.height / rw);
neww = width;
}
else
{
neww = round(mysize.width / rh);
newh = height;
}
Here's a way to approach the problem:
You know that either the image's height or width will be equal to that of the bounding box.
Once you've determined which dimension will equal the bounding box's, you use the image's aspect ratio to calculate the other dimension.
double sourceRatio = sourceImage.Width / sourceImage.Height;
double targetRatio = targetRect.Width / targetRect.Height;
Size finalSize;
if (sourceRatio > targetRatio)
{
finalSize = new Size(targetRect.Width, targetRect.Width / sourceRatio);
}
else
{
finalSize = new Size(targetRect.Height * sourceRatio, targetRect.Height);
}
$max_width = MAX_SIZE;
$max_height = MAX_SIZE;
if ($width >= $height) // with bigger than height
{
if ($width >= $max_width)
{
$new_width = $max_width;
$new_height = round($height*$max_width/$width); // scale in height
}
else
{
$new_width = $width; // smaller than max dimentions
$new_height = $height; // maintain dimentions
}
}
else // height bigger than width
{
if ($height >= $max_height)
{
$new_width = round($width*$max_height/$height); // scale in width
$new_height = $max_height;
}
else
{
$new_width = $width; // smaller than max dimentions
$new_height = $height; // maintain dimentions
}
}

How to "smart resize" a displayed image to original aspect ratio

I have an application in which end-users can size and position images in a designer. Since the spec calls for the image to be "stretched" to the containing control, the end user can end up with an awkwardly stretched image.
To help the user with image sizing I am thinking of implementing a smart resizer function which would allow the the user to easily fix the aspect ratio of the picture so that it no longer appears stretched.
The quick way to solve this is to actually provide two options: 1) scale from width 2) scale from height. The user chooses the method and the algorithm adjusts the size of the picture by using the original aspect ratio. For example: A picture is displayed as 200x200 on the designer but the original image is 1024x768 pixels. The user chooses "Smart Size from width" and the new size becomes ~200x150 since the original aspect ratio is ~1.333
That's OK, but how could I make the algorithm smarter and not bother the user by asking which dimension the recalculation should be based on?
If I'm interpreting your spec correctly, you want a result that is no larger than the one the end-user laid out originally; you want one of the two dimensions to shrink, and the other to stay the same. In other words, the new size should fill the designer space in one direction while shortening the size in the other direction to retain the original aspect ratio.
original_ratio = original_width / original_height
designer_ratio = designer_width / designer_height
if original_ratio > designer_ratio
designer_height = designer_width / original_ratio
else
designer_width = designer_height * original_ratio
Often you'll be working with integer coordinates, but the divisions to produce the ratios above need to be floating point. Here's a rearrangement of the formula to be more integer friendly. Make sure your integers have the range to handle the maximum width*height.
if original_width * designer_height > designer_width * original_height
designer_height = (designer_width * original_height) / original_width
else
designer_width = (designer_height * original_width) / original_height
Here's a solution I came up with when having to deal with this problem. Turned out pretty short and straightforward imo, just wanted to share it.
Handy "formulas": ratio = W / H → W = H * ratio → H = W / ratio
Calculate the ratio.
Calculate the height of the image if you would set the width to the new size width (maximum allowed width).
Calculate the width of the image if you would set the height to the new size height (maximum allowed height).
See which of the two sizes does not override the max size in any dimension. If the ratio is not 1 one of them will always be wrong and one will always be correct.
In Javascript
// Returns array with new width and height
function proportionalScale(originalSize, newSize)
{
var ratio = originalSize[0] / originalSize[1];
var maximizedToWidth = [newSize[0], newSize[0] / ratio];
var maximizedToHeight = [newSize[1] * ratio, newSize[1]];
if (maximizedToWidth[1] > newSize[1]) { return maximizedToHeight; }
else { return maximizedToWidth; }
}
originalSize and newSize is an array [0] = width, [1] = height
I also wanted to know this and all I saw were endless examples of scaling width OR height but could leave the other overflowing.
Resize width AND height without the need for a loop
Doesn't exceed the images original dimensions
.
private void ResizeImage(Image img, double maxWidth, double maxHeight)
{
double srcWidth = img.Source.Width;
double srcHeight = img.Source.Height;
double resizeWidth = srcWidth;
double resizeHeight = srcHeight;
double aspect = resizeWidth / resizeHeight;
if (resizeWidth > maxWidth)
{
resizeWidth = maxWidth;
resizeHeight = resizeWidth / aspect;
}
if (resizeHeight > maxHeight)
{
aspect = resizeWidth / resizeHeight;
resizeHeight = maxHeight;
resizeWidth = resizeHeight * aspect;
}
img.Width = resizeWidth;
img.Height = resizeHeight;
}
Calculate the new dimensions for both variants ("scale by width" and "scale by height"), then use the one that fits in the display.
Alternatively you could also calculate the the aspect ratio of the "bounding box" and compare it against the aspect ratio of the original image. Depending on which aspect ratio is larger, the height or the width needs to be scaled.
You also could restrict the resize process so that in all cases "scale by width" is done. Then, to change the size of the image, the user always has to change its width. The height will always be adjusted automatically.
Took the suggestion above and made it scale up/down within max height / width. Here the python code for it and also added support for rotating things while keeping within limites:
def _resize(image, dimensions, rotate=None):
"""
Resizes an image to be as close as possible to specified dimensions. Image is a
django image-model-field.
Will both scale up and down the image to meet this while keeping the proportions
in width and height
"""
if image and os.path.isfile(image.path):
im = pil.open(image.path)
logging.debug('resizing image from %s x %s --> %s x %s ' % (im.size[0], im.size[1], dimensions[0], dimensions[1]))
if rotate:
logging.debug('first rotating image %s' % rotate)
im = im.rotate(90)
srcWidth = Decimal(im.size[0])
srcHeight = Decimal(im.size[1])
resizeWidth = srcWidth
resizeHeight = srcHeight
aspect = resizeWidth / resizeHeight # Decimal
logging.debug('resize aspect is %s' % aspect)
if resizeWidth > dimensions[0] or resizeHeight > dimensions[1]:
# if width or height is bigger we need to shrink things
if resizeWidth > dimensions[0]:
resizeWidth = Decimal(dimensions[0])
resizeHeight = resizeWidth / aspect
if resizeHeight > dimensions[1] :
aspect = resizeWidth / resizeHeight
resizeHeight = Decimal(dimensions[1])
resizeWidth = resizeHeight * aspect
else:
# if both width and height are smaller we need to increase size
if resizeWidth < dimensions[0]:
resizeWidth = Decimal(dimensions[0])
resizeHeight = resizeWidth / aspect
if resizeHeight > dimensions[1] :
aspect = resizeWidth / resizeHeight
resizeHeight = Decimal(dimensions[1])
resizeWidth = resizeHeight * aspect
im = im.resize((resizeWidth, resizeHeight), pil.ANTIALIAS)
logging.debug('resized image to %s %s' % im.size)
im.save(image.path)
else:
# no action, due to no image or no image in path
pass
return image
Because you want to maximize showing as much as possible the scaled image (of the original) in your window, i.e. the area in your designer, you would take the larger of either the width or height of the original image, and scale that to 200. Pseudo-code (width, height are dimensions of original):
if (width > height) {
scaledWidth = 200;
scaledHeight = (height * 200) / width;
} else {
scaledHeight = 200;
scaledWidth = (width * 200) / height;
}
here is my solution,
a = aspect
sw = original image width
sh = original image height
dw = requested max width
dh = requested max height
sw and sh will contain the final resized values
code is PHP:
$a = $sw / $sh;
if($a > 1){
// wider image
if($sw != $dw){
$rt = $dw / $sw;
$sw = $sw * $rt;
$sh = $sh * $rt;
}
if($sh > $dh){
$rt = $dh / $sh;
$sw = $sw * $rt;
$sh = $sh * $rt;
}
}else{
// taller image
if($sh != $dh){
$rt = $dh / $sh;
$sh = $sh * $rt;
$sw = $sw * $rt;
}
if($sw > $dw){
$rt = $dw / $sw;
$sh = $sh * $rt;
$sw = $sw * $rt;
}
}
My solution to shrink and grow size in javascript based on https://stackoverflow.com/a/5654847/1055015
var scale = function (srcWidth, srcHeight, maxWidth, maxHeight) {
let resizeWidth = srcWidth;
let resizeHeight = srcHeight;
let aspect = resizeWidth / resizeHeight;
let scaleX = maxWidth / srcWidth;
let scaleY = maxHeight / srcHeight;
let scale = Math.min(scaleX, scaleY);
resizeWidth *= scale;
resizeHeight *= scale;
if (resizeWidth > maxWidth) {
resizeWidth = maxWidth;
resizeHeight = resizeWidth / aspect;
}
if (resizeHeight > maxHeight) {
aspect = resizeWidth / resizeHeight;
resizeHeight = maxHeight;
resizeWidth = resizeHeight * aspect;
}
return {
width : resizeWidth,
height: resizeHeight,
};
}
You just need to work out the scale needed for both dimensions and then take the smaller of the 2.
I used this way to resize the image to FHD way
if ( width >= height) {
var original_ratio = width / height
new_width = 1080 * original_ratio
console.log("new new_width = " + Math.round(new_width) );
console.log("new new_height = 1080");
} else {
var original_ratio = height / width
new_height = 1080 * original_ratio
console.log("new new_height = " + Math.round(new_height) );
console.log("new new_width = 1080");
}
you can change the 1080 to the new size.
I hope it's useful for someone at least.

Maintain the aspect ratio of an image?

I am using pictureBox to show images which are received from server but my problem is that picture box in compact framework has only three Size Modes
StretchImage, Normal, CenterImage
the pictures i am getting are generally bigger in size so i have to use StrecthImage mode. But then the aspect ratio is maintained so the images shown become distorted.
So is their anyway to come out of this problem ?
finally i found answer for my question which is here-----
float actualHeight = myImg.Height;
float actualWidth = myImg.Width;
float imgRatio = actualWidth / actualHeight;
float maxRatio = (float)this.Width / this.Height;
if(imgRatio!=maxRatio)
{
if (imgRatio < maxRatio)
{
imgRatio = this.Height / actualHeight;
actualWidth = imgRatio * actualWidth;
actualHeight = this.Height;
}
else
{
imgRatio = this.Width / actualWidth;
actualHeight = imgRatio * actualHeight;
actualWidth = this.Width;
}
}
pictureBox.Size=new Size((int)actualWidth,(int)actualHeight);
pictureBox.Location = new Point((int)((this.Width - actualWidth) / 2), (int)((this.Height - actualHeight) / 2));
but before doing this keep the picture box size mode as stretchImage

Scale image to completely fill bounding box

For instance, if I need to fill a bounding box that is 100px wide by 50px tall, the following input images would have the following behavior:
200w x 200h gets scaled down 50% and
25% gets chopped off the top and
bottom.
200w x 100h gets scaled down 50%
with no cropping.
100w x 200h gets is not scaled, but
75px get chopped off top and bottom.
This seems like it'd be a common resizing function, but I haven't been able to track down an example of the algorithm.
Will accept answer in any language including pseudo code. A link to a page with the answer is great too!
What you're asking for is pretty easy. Calculate the different scaling factors for the width and the height, then pick the larger one for your actual scale factor. Multiply your input size by the scale, and crop whichever one comes out too large.
scale = max(maxwidth/oldwidth, maxheight/oldheight)
scaledwidth = oldwidth * scale
scaledheight = oldheight * scale
if scaledheight > maxheight:
croptop = (scaledheight - maxheight) / 2
cropbottom = (scaledheight - maxheight) - croptop
if scaledwidth > maxwidth:
cropleft = (scaledwidth - maxwidth) / 2
cropright = (scaledwidth - maxwidth) - cropleft
Here we make sure that we only scale if X is greater than 100%; then after we've done that, we ensure that we are only 50 px on our Y. If we're greater than 50, then we take the difference and divide by 2 to get the amount removed from the top/bottom.
double percent_x = 1.0;
if(X > 100) {
percent_x = (float)100/X;
X *= percent_x;
Y *= percent_x;
}
int diff_y;
int top_cut, bott_cut;
if( Y > 50 ) {
diff_y = (Y - 50) / 2;
top_cut = bott_cut = diff_y;
}
Largely inspired by Mark Ransom's answer (thank you so much - you saved me). For anyone who would like to do this without cropping the image (just fit within the bounds), I've found that this works:
if (maxWidth > width && maxHeight > height) {
return { width, height };
}
aspectRatio = width / height,
scale = max(maxWidth / width, maxHeight / height);
scaledHeight = height * scale,
scaledWidth = width * scale;
if (scaledHeight > maxHeight) {
scaledHeight = maxHeight;
scaledWidth = aspectRatio * scaledHeight;
} else if (scaledWidth > maxWidth) {
scaledWidth = maxWidth;
scaledHeight = scaledWidth / aspectRatio;
}
return { scaledHeight, scaledWidth };

Resources