Skip to content

Commit

Permalink
Further improve image resolution strategy
Browse files Browse the repository at this point in the history
Now using multiple comparison steps instead of magic values
  • Loading branch information
Stypox committed May 2, 2023
1 parent ae7f369 commit bd3d6ef
Show file tree
Hide file tree
Showing 2 changed files with 69 additions and 39 deletions.
93 changes: 54 additions & 39 deletions app/src/main/java/org/schabi/newpipe/util/image/ImageStrategy.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.schabi.newpipe.util.image;

import static org.schabi.newpipe.extractor.Image.HEIGHT_UNKNOWN;
import static org.schabi.newpipe.extractor.Image.WIDTH_UNKNOWN;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

Expand All @@ -10,9 +13,10 @@

public final class ImageStrategy {

// the height thresholds also used by the extractor (TODO move them to the extractor)
private static final int LOW_MEDIUM = 175;
private static final int MEDIUM_HIGH = 720;
// when preferredImageQuality is LOW or MEDIUM, images are sorted by how close their preferred
// image quality is to these values (H stands for "Height")
private static final int BEST_LOW_H = 75;
private static final int BEST_MEDIUM_H = 250;

private static PreferredImageQuality preferredImageQuality = PreferredImageQuality.MEDIUM;

Expand All @@ -28,35 +32,17 @@ public static boolean shouldLoadImages() {
}


private static double estimatePixelCount(final Image image,
final double widthOverHeight,
final boolean unknownsLast) {
if (image.getHeight() == Image.HEIGHT_UNKNOWN) {
if (image.getWidth() == Image.WIDTH_UNKNOWN) {
switch (image.getEstimatedResolutionLevel()) {
case LOW:
return unknownsLast
? (LOW_MEDIUM - 1) * (LOW_MEDIUM - 1) * widthOverHeight
: 0;
case MEDIUM:
return unknownsLast
? (MEDIUM_HIGH - 1) * (MEDIUM_HIGH - 1) * widthOverHeight
: LOW_MEDIUM * LOW_MEDIUM * widthOverHeight;
case HIGH:
return unknownsLast
? 1e20 // less than 1e21 to prefer over fully unknown image sizes
: MEDIUM_HIGH * MEDIUM_HIGH * widthOverHeight;
default:
case UNKNOWN:
// images whose size is completely unknown will be avoided when possible
return unknownsLast ? 1e21 : -1;
}
private static double estimatePixelCount(final Image image, final double widthOverHeight) {
if (image.getHeight() == HEIGHT_UNKNOWN) {
if (image.getWidth() == WIDTH_UNKNOWN) {
// images whose size is completely unknown will be avoided when possible
return -10e20;

} else {
return image.getWidth() * image.getWidth() / widthOverHeight;
}

} else if (image.getWidth() == Image.WIDTH_UNKNOWN) {
} else if (image.getWidth() == WIDTH_UNKNOWN) {
return image.getHeight() * image.getHeight() * widthOverHeight;

} else {
Expand All @@ -70,35 +56,64 @@ public static String choosePreferredImage(@NonNull final List<Image> images) {
return null; // do not load images
}

// this will be used to estimate the pixel count for images where only one of height or
// width are known
final double widthOverHeight = images.stream()
.filter(image -> image.getHeight() != Image.HEIGHT_UNKNOWN
&& image.getWidth() != Image.WIDTH_UNKNOWN)
.filter(image -> image.getHeight() != HEIGHT_UNKNOWN
&& image.getWidth() != WIDTH_UNKNOWN)
.mapToDouble(image -> ((double) image.getWidth()) / image.getHeight())
.findFirst()
.orElse(1.0);

final Comparator<Image> comparator;
final Image.ResolutionLevel preferredLevel = preferredImageQuality.toResolutionLevel();
Comparator<Image> comparator = Comparator
// the first step splits the images into groups of resolution levels
.<Image>comparingInt(i -> {
if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.UNKNOWN) {
return 3; // avoid unknowns as much as possible
} else if (i.getEstimatedResolutionLevel() == preferredLevel) {
return 0; // prefer a matching resolution level
} else if (i.getEstimatedResolutionLevel() == Image.ResolutionLevel.MEDIUM) {
return 1; // the preferredLevel is only 1 "step" away (either HIGH or LOW)
} else {
return 2; // the preferredLevel is the furthest away possible (2 "steps")
}
})
// then each level's group is further split into two subgroups, one with known image
// size (which is also the preferred subgroup) and the other without
.thenComparing((a, b) -> Boolean.compare(
a.getHeight() != HEIGHT_UNKNOWN || a.getWidth() != WIDTH_UNKNOWN,
b.getHeight() != HEIGHT_UNKNOWN || b.getWidth() != WIDTH_UNKNOWN
));

// The third step chooses, within each subgroup with known image size, the best image based
// on how close its size is to BEST_LOW_H or BEST_MEDIUM_H (with proper units). Subgroups
// without known image size will be left untouched since estimatePixelCount always returns
// the same number for those.
switch (preferredImageQuality) {
case LOW:
comparator = Comparator.comparingDouble(
image -> estimatePixelCount(image, widthOverHeight, true));
comparator = comparator.thenComparingDouble(image -> {
final double pixelCount = estimatePixelCount(image, widthOverHeight);
return Math.abs(pixelCount - BEST_LOW_H * BEST_LOW_H * widthOverHeight);
});
break;
default:
case MEDIUM:
comparator = Comparator.comparingDouble(image -> {
final double pixelCount = estimatePixelCount(image, widthOverHeight, true);
final double mediumHeight = (LOW_MEDIUM + MEDIUM_HIGH) / 2.0;
return Math.abs(pixelCount - mediumHeight * mediumHeight * widthOverHeight);
comparator = comparator.thenComparingDouble(image -> {
final double pixelCount = estimatePixelCount(image, widthOverHeight);
return Math.abs(pixelCount - BEST_MEDIUM_H * BEST_MEDIUM_H * widthOverHeight);
});
break;
case HIGH:
comparator = Comparator.<Image>comparingDouble(
image -> estimatePixelCount(image, widthOverHeight, false))
.reversed();
comparator = comparator.thenComparingDouble(
// this is reversed with a - so that the highest resolution is chosen
i -> -estimatePixelCount(i, widthOverHeight));
break;
}

return images.stream()
// using "min" basically means "take the first group, then take the first subgroup,
// then choose the best image, while ignoring all other groups and subgroups"
.min(comparator)
.map(Image::getUrl)
.orElse(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import android.content.Context;

import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.Image;

public enum PreferredImageQuality {
NONE,
Expand All @@ -21,4 +22,18 @@ public static PreferredImageQuality fromPreferenceKey(final Context context, fin
return MEDIUM; // default to medium
}
}

public Image.ResolutionLevel toResolutionLevel() {
switch (this) {
case LOW:
return Image.ResolutionLevel.LOW;
case MEDIUM:
return Image.ResolutionLevel.MEDIUM;
case HIGH:
return Image.ResolutionLevel.HIGH;
default:
case NONE:
return Image.ResolutionLevel.UNKNOWN;
}
}
}

0 comments on commit bd3d6ef

Please sign in to comment.