基於.NetCore開發部落格專案 StarBlog - (15) 生成隨機尺寸圖片

程式設計實驗室發表於2022-07-08

系列文章

前言

之前我寫了一篇 .NetCore實現圖片縮放與裁剪 - 基於ImageSharp,裡面有生成尺寸隨機圖片的演算法,同時也是StarBlog部落格中原有的實現方式,不過偶爾重新整理頁面的時候我注意到有些圖片載入不出來,除錯了一下發現原來是報錯了,原本這個演算法有bug。於是利用週末時間重新實現了一遍,這下可以說是完美了~

生成隨機尺寸圖片的功能目前用在文章卡片上,原本使用的是LoremPicsum提供的服務,但它的伺服器在國外,上線之後發現載入太慢了,經常載入不出來,於是決定自己實現一版。功能基礎是上文提到的文章中的ImageSharp。

image

思路

先理一下需求

  • 指定一個目錄作為圖片庫位置,把之前蒐集的桌布圖片放進去,大概放個幾百張就行了吧
  • 遍歷獲取到庫中的圖片列表
  • 隨機取出一張圖片
  • 根據指定的尺寸縮放或裁剪圖片

其他的還有諸如指定隨機seed、將seed與圖片進行靜態對映等擴充套件功能的實現。

關鍵功能

關鍵功能在於「根據指定的尺寸縮放或裁剪圖片」

難點在於裁剪和縮放圖片要保證:

  • 不改變圖片原有的比例
  • 儘量保持圖片原有的內容元素

第一版我是將橫屏和豎屏的圖片分開處理,(在輸入尺寸不超過原圖尺寸的情況下)先把比例接近的邊調整成一樣的大小,再裁剪中間部分,不過問題也很明顯,如果調整大小之後另一條邊的長度小於輸入長度,那就會拉伸圖片,導致比例改變。

在參考幾個類似的MATLAB和Python專案之後,我換了別的思路:

  1. 在輸入尺寸不超過原圖尺寸的情況下,先按輸入的尺寸比例裁剪、再調整尺寸
  2. 如果超出原圖尺寸,則先按比例調整原圖的大小,再重複第一步

舉個例子

比如原圖是 1080 x 2340 的尺寸,輸入的圖片是 400 x 200 尺寸

那第一步判斷尺寸不超過原圖,不需要縮放

然後是「按輸入的尺寸比例裁剪」,把 400 x 200 化簡成 2 : 1 的比例

在原圖中擷取 2 : 1 的大小,即 1080 x 540

然後再把擷取的圖片調整到 400 x 200,搞定!

看下效果

原圖 輸出(400x200) 輸出(200x300)
image image image

雖然比一開始的方案更費一丟丟記憶體,但卻實實在在提升了出圖成功率,nice~

程式碼實現

直接上程式碼好了,根據上面提到的思路,分兩步走,程式碼也比一開始的方案更整潔

async Task<(Image, IImageFormat)> GenerateSizedImageAsync(string imagePath, int width, int height) {
        await using var fileStream = new FileStream(imagePath, FileMode.Open);
        var (image, format) = await Image.LoadWithFormatAsync(fileStream);
        
        // 輸出尺寸超出原圖片尺寸,放大
        if (width > image.Width && height > image.Height) {
            image.Mutate(a => a.Resize(width, height));
        }
        else if (width > image.Width || height > image.Height) {
            // 改變比例大的邊
            if (width / image.Width < height / image.Height)
                image.Mutate(a => a.Resize(0, height));
            else
                image.Mutate(a => a.Resize(width, 0));
        }
        
        // 將輸入的尺寸作為裁剪比例
        var (scaleWidth, scaleHeight) = GetPhotoScale(width, height);
        var cropWidth = image.Width;
        var cropHeight = (int) (image.Width / scaleWidth * scaleHeight);
        if (cropHeight > image.Height) {
            cropHeight = image.Height;
            cropWidth = (int) (image.Height / scaleHeight * scaleWidth);
        }

        var cropRect = new Rectangle((image.Width - cropWidth) / 2, (image.Height - cropHeight) / 2, cropWidth, cropHeight);
        image.Mutate(a => a.Crop(cropRect));
        image.Mutate(a => a.Resize(width, height));

        return (image, format);
    }

裡面還用到了計算圖片比例,很簡單,先算出圖片寬度和高度的最大公約數,然後寬高分別除以這個最大公約數,就是比例了(也就是化簡分數)

計算最大公約數程式碼

private static int GetGreatestCommonDivisor(int m, int n) {
    if (m < n) (n, m) = (m, n);

    while (n != 0) {
        var r = m % n;
        m = n;
        n = r;
    }
    return m;
}

計算圖片比例程式碼

private static (double, double) GetPhotoScale(int width, int height) {
    if (width == height) return (1, 1);
    var gcd = GetGreatestCommonDivisor(width, height);
    return ((double)width / gcd, (double)height / gcd);
}

參考資料

相關文章