午後わてんのブログ

ベランダ菜園とWindows用アプリ作成(WPFとC#)

C#、WPF、ランチョス補完法での画像リサイズ処理に再挑戦、グレースケール専用

今回の記事も3年前の再挑戦

f:id:gogowaten:20210427120953p:plain
ランチョス法での重みのグラフ
グラフタイトルのかっこいいフォントはこちらのを使用
ACT SELECT [ZILLION] REPLICA FONT
http://actselect.chips.jp/fonts/32.htm

ぬか喜びからの修正

ランチョス法もバイキュービック法と同様に重み計算式があるから、前回までのバイキュービック法で使ったコードの重み計算のところを、ランチョス法にすればいいだけかと思って書いたのがこれなんだけど

不具合のあるコード

//窓関数
private double Sinc(double d)
{
    return Math.Sin(Math.PI * d) / (Math.PI * d);
}
/// <summary>
/// ランチョス補完法での重み計算
/// </summary>
/// <param name="d">距離</param>
/// <param name="n">最大参照距離</param>
/// <returns></returns>
private double GetLanczosWeight(double d, int n)
{
    if (d == 0) return 1.0;
    else if (d > n) return 0.0;
    else return Sinc(d) * Sinc(d / n);
}

//未使用
/// <summary>
/// 画像の拡大縮小、ランチョス法で補完、PixelFormats.Gray8専用)
/// 不具合版
/// </summary>
/// <param name="source">PixelFormats.Gray8のBitmap</param>
/// <param name="width">変換後の横ピクセル数を指定</param>
/// <param name="height">変換後の縦ピクセル数を指定</param>
/// <param name="n">最大参照距離、3か4がいい</param>
/// <returns></returns>
private BitmapSource LanczosGray8(BitmapSource source, int width, int height, int n)
{
    {
        //1ピクセルあたりのバイト数、Byte / Pixel
        int pByte = (source.Format.BitsPerPixel + 7) / 8;

        //元画像の画素値の配列作成
        int sourceWidth = source.PixelWidth;
        int sourceHeight = source.PixelHeight;
        int sourceStride = sourceWidth * pByte;//1行あたりのbyte数
        byte[] sourcePixels = new byte[sourceHeight * sourceStride];
        source.CopyPixels(sourcePixels, sourceStride, 0);

        //変換後の画像の画素値の配列用
        double widthScale = (double)sourceWidth / width;//横倍率
        double heightScale = (double)sourceHeight / height;
        int stride = width * pByte;
        byte[] pixels = new byte[height * stride];

        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                //参照点
                double rx = (x + 0.5) * widthScale;
                double ry = (y + 0.5) * heightScale;
                //参照点四捨五入で基準
                int xKijun = (int)(rx + 0.5);
                int yKijun = (int)(ry + 0.5);

                double vv = 0;
                //参照範囲は基準から上(左)へn、下(右)へn-1の範囲
                for (int yy = -n; yy < n; yy++)
                //for (int yy = -2; yy <= 1; yy++)
                {
                    //+0.5しているのは中心座標で計算するため
                    double dy = Math.Abs(ry - (yy + yKijun + 0.5));//距離
                    double yw = GetLanczosWeight(dy, n);//重み
                    int yc = yKijun + yy;
                    //マイナス座標や画像サイズを超えていたら、収まるように修正
                    yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                    for (int xx = -n; xx < n; xx++)
                    //for (int xx = -2; xx <= 1; xx++)
                    {
                        double dx = Math.Abs(rx - (xx + xKijun + 0.5));
                        double xw = GetLanczosWeight(dx, n);
                        int xc = xKijun + xx;
                        xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                        double weight = yw * xw;
                        int pp = (yc * sourceStride) + (xc * pByte);
                        vv += sourcePixels[pp] * weight;
                    }
                }
                //0~255の範囲を超えることがあるので、修正
                vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                int ap = (y * stride) + (x * pByte);
                pixels[ap] = (byte)(vv + 0.5);
            }
        };

        BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride);
        return bitmap;
    }
}


f:id:gogowaten:20210427120459p:plain
ランチョス法の重み計算
これをC#で書いたのが
f:id:gogowaten:20210427111737p:plain
ランチョス
ランチョスによる重み計算、この部分は合っているはずで、問題があるのはこれを使う部分

f:id:gogowaten:20210427112011p:plain
問題がある部分
バイキュービック法のコードから変更した部分は2箇所だけで、コメントアウトしてある433行目と、442行目。バイキュービック法での参照範囲は2x2の固定だったけど、ランチョス法では2以上を任意で指定するので変数に変えた
これでいいはずと思ってテストしてみたら

f:id:gogowaten:20210424200647j:plain
テスト画像
これを1/2に縮小

f:id:gogowaten:20210427112925p:plain
縮小したところ
できた!

f:id:gogowaten:20210427113119p:plain
1/3
1/3もできてる、意外ににかんたんだったなあ

ところが

f:id:gogowaten:20210427113449p:plain
参照範囲n=3とn=2
参照範囲を変えて変換したら色(輝度)が変化した、n=2のほうが全体的に少し明るい
画像を並べたくらいでは気づかないけど、画像が切り替わると気づく、それくらいの違い
最初はnの値でそうなるのかなあとか思ったけど

輝度200の1色画像でテスト

f:id:gogowaten:20210427114207p:plain
輝度200画像

f:id:gogowaten:20210427114913p:plain
輝度値の変化
n=2では208と元画像より明るくなって、n=3では逆に198と暗くなっていた、どちらにしても間違っている


ランチョス法での重み計算は修正が必要?

気づいたこと

色(輝度)が変化するってことは重み計算が間違っているんだよねえ、でもコードを見直しても間違っていないはずで、じゃあ使い方が間違っているってことになる、でもわからん

バイリニアとバイキュービックの重みグラフを作るときに使った表を見ていて気づくのが、0~最大距離の重さの合計は常に5.5

f:id:gogowaten:20210427122757p:plain
バイリニア

f:id:gogowaten:20210427122810p:plain
バイキュービック

ランチョス法では

f:id:gogowaten:20210427123043p:plain
ランチョス法 n=2

f:id:gogowaten:20210427123111p:plain
ランチョス法 n=3
n=2では5.5を超えていて、n=3では下回っている
これは輝度の変化と同じ傾向、なにかあるってことで



重みの修正

実際に計算してみる
n=2、つまり参照距離は最大で2、範囲だと上下左右なので2x2の範囲のとき

f:id:gogowaten:20210427123622p:plain
参照点r(2.8,2.3)
参照点rが(2.8,2.3)のときのx軸だけで見てみる
f:id:gogowaten:20210427124516p:plain
重みの修正
rのxは2.8、右隣13のxは3.5、rから13までの距離は3.5-2.8=0.7
11,12,14までの距離も適当に計算して、距離からランチョス法で重みを計算して4つを合計したら1.0129になった、1(100%)を超えている、これは良くない
全体で1にするには、各重みを重み合計値で割り算、これで修正できる

すべてのピクセルの輝度が200のとき、修正しないで計算すると

f:id:gogowaten:20210427125236p:plain
結果
元の輝度値を超えて202.58になってしまう、これが原因だねえ

ランチョス法の計算式の部分はそれほど複雑でもなく短いコードだし、エクセルで確認しても間違ってなさそう、でも1以外になる、じゃあランチョス法はそういうものだと思って、計算の最後に1になるように修正しようってことにした
この方法が合っているのかはわからんけど
修正版のコードは次

コード

//窓関数
private double Sinc(double d)
{
    return Math.Sin(Math.PI * d) / (Math.PI * d);
}
/// <summary>
/// ランチョス補完法での重み計算
/// </summary>
/// <param name="d">距離</param>
/// <param name="n">最大参照距離</param>
/// <returns></returns>
private double GetLanczosWeight(double d, int n)
{
    if (d == 0) return 1.0;
    else if (d > n) return 0.0;
    else return Sinc(d) * Sinc(d / n);
}

/// <summary>
/// 画像の拡大縮小、ランチョス法で補完、PixelFormats.Gray8専用)
/// 不具合修正版
/// </summary>
/// <param name="source">PixelFormats.Gray8のBitmap</param>
/// <param name="width">変換後の横ピクセル数を指定</param>
/// <param name="height">変換後の縦ピクセル数を指定</param>
/// <param name="n">最大参照距離、3か4がいい</param>
/// <returns></returns>
private BitmapSource LanczosGray8Ex(BitmapSource source, int width, int height, int n)
{
    //1ピクセルあたりのバイト数、Byte / Pixel
    int pByte = (source.Format.BitsPerPixel + 7) / 8;

    //元画像の画素値の配列作成
    int sourceWidth = source.PixelWidth;
    int sourceHeight = source.PixelHeight;
    int sourceStride = sourceWidth * pByte;//1行あたりのbyte数
    byte[] sourcePixels = new byte[sourceHeight * sourceStride];
    source.CopyPixels(sourcePixels, sourceStride, 0);

    //変換後の画像の画素値の配列用
    double widthScale = (double)sourceWidth / width;//横倍率
    double heightScale = (double)sourceHeight / height;
    int stride = width * pByte;
    byte[] pixels = new byte[height * stride];

    for (int y = 0; y < height; y++)
    {
        for (int x = 0; x < width; x++)
        {
            //参照点
            double rx = (x + 0.5) * widthScale;
            double ry = (y + 0.5) * heightScale;
            //参照点四捨五入で基準
            int xKijun = (int)(rx + 0.5);
            int yKijun = (int)(ry + 0.5);
            //修正した重み取得
            var ws = GetFixWeights(rx, ry, n);

            double sum = 0;
            //参照範囲は基準から上(xは左)へn、下(xは右)へn-1の範囲
            for (int yy = -n; yy < n; yy++)
            {
                int yc = yKijun + yy;
                //マイナス座標や画像サイズを超えていたら、収まるように修正
                yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                for (int xx = -n; xx < n; xx++)
                {
                    int xc = xKijun + xx;
                    xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                    int pp = (yc * sourceStride) + (xc * pByte);
                    sum += sourcePixels[pp] * ws[xx + n, yy + n];
                }
            }
            //0~255の範囲を超えることがあるので、修正
            sum = sum < 0 ? 0 : sum > 255 ? 255 : sum;
            int ap = (y * stride) + (x * pByte);
            pixels[ap] = (byte)(sum + 0.5);
        }
    };

    BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride);
    return bitmap;

    //修正した重み取得
    double[,] GetFixWeights(double rx, double ry, int n)
    {
        int nn = n * 2;//全体の参照距離
        //基準になる距離計算
        double sx = rx - (int)rx;
        double sy = ry - (int)ry;
        double dx = (sx < 0.5) ? 0.5 - sx : 0.5 - sx + 1;
        double dy = (sy < 0.5) ? 0.5 - sy : 0.5 - sy + 1;

        //各ピクセルの重みと、重み合計を計算
        double[] xw = new double[nn];
        double[] yw = new double[nn];
        double xSum = 0, ySum = 0;
        for (int i = -n; i < n; i++)
        {
            double x = GetLanczosWeight(Math.Abs(dx + i), n);
            xSum += x;
            xw[i + n] = x;
            double y = GetLanczosWeight(Math.Abs(dy + i), n);
            ySum += y;
            yw[i + n] = y;
        }

        //重み合計で割り算して修正、全体で100%(1.0)にする
        for (int i = 0; i < nn; i++)
        {
            xw[i] /= xSum;
            yw[i] /= ySum;
        }

        // x * y
        double[,] ws = new double[nn, nn];
        for (int y = 0; y < nn; y++)
        {
            for (int x = 0; x < nn; x++)
            {
                ws[x, y] = xw[x] * yw[y];
            }
        }
        return ws;
    }
}


f:id:gogowaten:20210427131043p:plain
修正処理部分
これはさっきのエクセルで計算していた
f:id:gogowaten:20210427124516p:plain
修正処理部分
これのところ、x,yそれぞれを計算して、修正した後、x*yした一覧表を作成している
これを使うところは
f:id:gogowaten:20210427131644p:plain
修正した重みを使う
316行目で修正した重みを取得して、330行目で輝度値に掛け算している


テストアプリ

f:id:gogowaten:20210427133112p:plain
テストアプリ

  • 画像ファイルドロップかクリップボードからの貼り付けで画像表示
  • 倍率は1~10
  • 倍率=2で縮小は1/2、倍率=5で縮小は1/5
  • グレースケール専用
  • 改がついているボタンはマルチスレッド+セパラブルを使って処理、速い
  • n=3が標準参照距離、大きくすると処理時間かかる
  • 左下に処理時間表示
  • 処理中は操作不能
  • 空きメモリが2GB以下と少ないときに、大きな画像(1000x1000以上)を10倍拡大処理するとアプリが落ちる


作成動作環境

ダウンロード

github.com

ここの20210426_Lanczos.zip


コード

github.com

MainWindow.xaml

<Window x:Class="_20210426_Lanczosで拡大縮小.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
        xmlns:local="clr-namespace:_20210426_Lanczosで拡大縮小"
        mc:Ignorable="d"
        Title="MainWindow" Height="487" Width="634"
        AllowDrop="True" Drop="Window_Drop">
  <Window.Resources>
    <Style TargetType="Button">
      <Setter Property="Margin" Value="2,10,2,0"/>
    </Style>
  </Window.Resources>
  <Grid>
    <DockPanel UseLayoutRounding="True">
      <StatusBar DockPanel.Dock="Bottom">
        <StatusBarItem x:Name="MyStatusItem" Content="time"/>
      </StatusBar>
      <StackPanel DockPanel.Dock="Right" Background="White">
        <StackPanel>
          <Slider x:Name="MySliderScale" Minimum="1" Maximum="10" Value="2" Width="80"
                  HorizontalAlignment="Center" TickFrequency="1" IsSnapToTickEnabled="True"
                  MouseWheel="MySlider_MouseWheel" SmallChange="1" LargeChange="1">
            <Slider.LayoutTransform>
              <RotateTransform Angle="270"/>
            </Slider.LayoutTransform>
          </Slider>
          <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
            <TextBlock Text="倍率="/>
            <TextBlock Text="{Binding ElementName=MySliderScale, Path=Value}"/>
          </StackPanel>
        </StackPanel>
        <Button x:Name="MyButton1" Content="縮小(8bit)" Click="MyButton1_Click"/>
        <Button x:Name="MyButton2" Content="拡大(8bit)" Click="MyButton2_Click"/>
        <Button x:Name="MyButton3" Content="縮小改(8bit)" Click="MyButton3_Click"/>
        <Button x:Name="MyButton4" Content="拡大改(8bit)" Click="MyButton4_Click"/>

        <Button x:Name="MyButtonToOrigin" Content="戻す" Click="MyButtonToOrigin_Click"/>
        <Slider x:Name="MySlider" Minimum="2" Maximum="6" SmallChange="1" TickFrequency="1" IsSnapToTickEnabled="True"
                Value="3" Width="100" HorizontalAlignment="Center" MouseWheel="MySlider_MouseWheel">
          <Slider.LayoutTransform>
            <RotateTransform Angle="270"/>
          </Slider.LayoutTransform>
        </Slider>
        <TextBlock Text="{Binding ElementName=MySlider, Path=Value, StringFormat=n\=0}"
                   HorizontalAlignment="Center"/>
        <Button x:Name="MyButtonCopy" Content="コピ" Click="MyButtonCopy_Click"/>
        <Button x:Name="MyButtonPaste" Content="ペ" Click="MyButtonPaste_Click"/>
      </StackPanel>
      <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <Image x:Name="MyImage" Stretch="None"/>
      </ScrollViewer>
    </DockPanel>
  </Grid>
</Window>




MainWindow.xaml.cs

using System;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;

using System.Diagnostics;


namespace _20210426_Lanczosで拡大縮小
{
    public partial class MainWindow : Window
    {
        private BitmapSource MyBitmapOrigin;
        //private BitmapSource MyBitmapOrigin32bit;
        public MainWindow()
        {
            InitializeComponent();
        }

        //処理時間計測
        private void MyExe(Func<BitmapSource, int, int, int, BitmapSource> func,
    BitmapSource source, int width, int height, int a)
        {
            var sw = new Stopwatch();
            sw.Start();
            var bitmap = func(source, width, height, a);
            sw.Stop();
            MyStatusItem.Content = $"処理時間:{sw.Elapsed.TotalSeconds:000.000}秒, {func.Method.Name}";
            MyImage.Source = bitmap;
        }


        //窓関数
        private double Sinc(double d)
        {
            return Math.Sin(Math.PI * d) / (Math.PI * d);
        }
        /// <summary>
        /// ランチョス補完法での重み計算
        /// </summary>
        /// <param name="d">距離</param>
        /// <param name="n">最大参照距離</param>
        /// <returns></returns>
        private double GetLanczosWeight(double d, int n)
        {
            if (d == 0) return 1.0;
            else if (d > n) return 0.0;
            else return Sinc(d) * Sinc(d / n);
        }



        /// <summary>
        /// 画像の拡大縮小、ランチョス法で補完、PixelFormats.Gray8専用)
        /// 修正版、セパラブルとParallelで高速化
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="n">最大参照距離、3か4がいい</param>
        /// <returns></returns>
        private BitmapSource LanczosGray8KaiEx(BitmapSource source, int width, int height, int n)
        {
            //1ピクセルあたりのバイト数、Byte / Pixel
            int pByte = (source.Format.BitsPerPixel + 7) / 8;

            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int sourceStride = sourceWidth * pByte;//1行あたりのbyte数
            byte[] sourcePixels = new byte[sourceHeight * sourceStride];
            source.CopyPixels(sourcePixels, sourceStride, 0);

            //変換後の画像の画素値の配列用
            double widthScale = (double)sourceWidth / width;//横倍率
            double heightScale = (double)sourceHeight / height;
            int stride = width * pByte;
            byte[] pixels = new byte[height * stride];

            //横処理用配列
            double[] xResult = new double[sourceHeight * stride];

            //横処理
            _ = Parallel.For(0, sourceHeight, y =>
              {
                  for (int x = 0; x < width; x++)
                  {
                      //参照点
                      double rx = (x + 0.5) * widthScale;
                      //参照点四捨五入で基準
                      int xKijun = (int)(rx + 0.5);
                      //修正した重み取得
                      double[] ws = GetFixWeihgts(rx, n);

                      double sum = 0;
                      int pp;
                      for (int xx = -n; xx < n; xx++)
                      {
                          int xc = xKijun + xx;
                          //マイナス座標や画像サイズを超えていたら、収まるように修正
                          xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                          pp = (y * sourceStride) + (xc * pByte);
                          sum += sourcePixels[pp] * ws[xx + n];
                      }
                      pp = y * stride + x * pByte;
                      xResult[pp] = sum;
                  }
              });

            //縦処理
            _ = Parallel.For(0, height, y =>
              {
                  for (int x = 0; x < width; x++)
                  {
                      double ry = (y + 0.5) * heightScale;
                      int yKijun = (int)(ry + 0.5);

                      double[] ws = GetFixWeihgts(ry, n);
                      double sum = 0;
                      int pp;
                      for (int yy = -n; yy < n; yy++)
                      {
                          int yc = yKijun + yy;
                          yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                          pp = (yc * stride) + (x * pByte);
                          sum += xResult[pp] * ws[yy + n];
                      }
                      //0~255の範囲を超えることがあるので、修正
                      sum = sum < 0 ? 0 : sum > 255 ? 255 : sum;
                      int ap = (y * stride) + (x * pByte);
                      pixels[ap] = (byte)(sum + 0.5);
                  }
              });


            BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride);
            return bitmap;

            //修正した重み取得
            double[] GetFixWeihgts(double r, int n)
            {
                int nn = n * 2;//全体の参照距離
                //基準距離
                double s = r - (int)r;
                double d = (s < 0.5) ? 0.5 - s : 0.5 - s + 1;

                //各重みと重み合計
                double[] ws = new double[nn];
                double sum = 0;
                for (int i = -n; i < n; i++)
                {
                    double w = GetLanczosWeight(Math.Abs(d + i), n);
                    sum += w;
                    ws[i + n] = w;
                }

                //重み合計で割り算して修正、全体で100%(1.0)にする
                for (int i = 0; i < nn; i++)
                {
                    ws[i] /= sum;
                }
                return ws;
            }
        }




        
        /// <summary>
        /// 画像の拡大縮小、ランチョス法で補完、PixelFormats.Gray8専用)
        /// 不具合修正版
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="n">最大参照距離、3か4がいい</param>
        /// <returns></returns>
        private BitmapSource LanczosGray8Ex(BitmapSource source, int width, int height, int n)
        {
            //1ピクセルあたりのバイト数、Byte / Pixel
            int pByte = (source.Format.BitsPerPixel + 7) / 8;

            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int sourceStride = sourceWidth * pByte;//1行あたりのbyte数
            byte[] sourcePixels = new byte[sourceHeight * sourceStride];
            source.CopyPixels(sourcePixels, sourceStride, 0);

            //変換後の画像の画素値の配列用
            double widthScale = (double)sourceWidth / width;//横倍率
            double heightScale = (double)sourceHeight / height;
            int stride = width * pByte;
            byte[] pixels = new byte[height * stride];

            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * widthScale;
                    double ry = (y + 0.5) * heightScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);
                    int yKijun = (int)(ry + 0.5);
                    //修正した重み取得
                    var ws = GetFixWeights(rx, ry, n);

                    double sum = 0;
                    //参照範囲は基準から上(xは左)へn、下(xは右)へn-1の範囲
                    for (int yy = -n; yy < n; yy++)
                    {
                        int yc = yKijun + yy;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        for (int xx = -n; xx < n; xx++)
                        {
                            int xc = xKijun + xx;
                            xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                            int pp = (yc * sourceStride) + (xc * pByte);
                            sum += sourcePixels[pp] * ws[xx + n, yy + n];
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    sum = sum < 0 ? 0 : sum > 255 ? 255 : sum;
                    int ap = (y * stride) + (x * pByte);
                    pixels[ap] = (byte)(sum + 0.5);
                }
            };

            BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride);
            return bitmap;

            //修正した重み取得
            double[,] GetFixWeights(double rx, double ry, int n)
            {
                int nn = n * 2;//全体の参照距離
                //基準になる距離計算
                double sx = rx - (int)rx;
                double sy = ry - (int)ry;
                double dx = (sx < 0.5) ? 0.5 - sx : 0.5 - sx + 1;
                double dy = (sy < 0.5) ? 0.5 - sy : 0.5 - sy + 1;

                //各ピクセルの重みと、重み合計を計算
                double[] xw = new double[nn];
                double[] yw = new double[nn];
                double xSum = 0, ySum = 0;
                for (int i = -n; i < n; i++)
                {
                    double x = GetLanczosWeight(Math.Abs(dx + i), n);
                    xSum += x;
                    xw[i + n] = x;
                    double y = GetLanczosWeight(Math.Abs(dy + i), n);
                    ySum += y;
                    yw[i + n] = y;
                }

                //重み合計で割り算して修正、全体で100%(1.0)にする
                for (int i = 0; i < nn; i++)
                {
                    xw[i] /= xSum;
                    yw[i] /= ySum;
                }

                // x * y
                double[,] ws = new double[nn, nn];
                for (int y = 0; y < nn; y++)
                {
                    for (int x = 0; x < nn; x++)
                    {
                        ws[x, y] = xw[x] * yw[y];
                    }
                }
                return ws;
            }
        }




        /// <summary>
        /// 画像ファイルパスからPixelFormats.Gray8のBitmapSource作成
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="dpiX"></param>
        /// <param name="dpiY"></param>
        /// <returns></returns>
        private BitmapSource MakeBitmapSourceGray8FromFile(string filePath, double dpiX = 96, double dpiY = 96)
        {
            BitmapSource source = null;
            try
            {
                using (var stream = System.IO.File.OpenRead(filePath))
                {
                    source = BitmapFrame.Create(stream);
                    if (source.Format != PixelFormats.Gray8)
                    {
                        source = new FormatConvertedBitmap(source, PixelFormats.Gray8, null, 0);
                    }
                    int w = source.PixelWidth;
                    int h = source.PixelHeight;
                    int stride = (w * source.Format.BitsPerPixel + 7) / 8;
                    byte[] pixels = new byte[h * stride];
                    source.CopyPixels(pixels, stride, 0);
                    source = BitmapSource.Create(w, h, dpiX, dpiY, source.Format, source.Palette, pixels, stride);
                };
            }
            catch (Exception)
            { }
            return source;
        }


        #region コピペ

        //        クリップボードに複数の形式のデータをコピーする - .NET Tips(VB.NET, C#...)
        //https://dobon.net/vb/dotnet/system/clipboardmultidata.html
        //        アルファ値を失わずに画像のコピペできた、.NET WPFのClipboard - 午後わてんのブログ
        //https://gogowaten.hatenablog.com/entry/2021/02/10/134406
        /// <summary>
        /// BitmapSourceをPNG形式に変換したものと、そのままの形式の両方をクリップボードにコピーする
        /// </summary>
        /// <param name="source"></param>
        private void ClipboardSetImageWithPng(BitmapSource source)
        {
            //DataObjectに入れたいデータを入れて、それをクリップボードにセットする
            DataObject data = new();

            //BitmapSource形式そのままでセット
            data.SetData(typeof(BitmapSource), source);

            //PNG形式にエンコードしたものをMemoryStreamして、それをセット
            //画像をPNGにエンコード
            PngBitmapEncoder pngEnc = new();
            pngEnc.Frames.Add(BitmapFrame.Create(source));
            //エンコードした画像をMemoryStreamにSava
            using var ms = new System.IO.MemoryStream();
            pngEnc.Save(ms);
            data.SetData("PNG", ms);

            //クリップボードにセット
            Clipboard.SetDataObject(data, true);

        }


        /// <summary>
        /// クリップボードからBitmapSourceを取り出して返す、PNG(アルファ値保持)形式に対応
        /// </summary>
        /// <returns></returns>
        private BitmapSource GetImageFromClipboardWithPNG()
        {
            BitmapSource source = null;
            //クリップボードにPNG形式のデータがあったら、それを使ってBitmapFrame作成して返す
            //なければ普通にClipboardのGetImage、それでもなければnullを返す
            using var ms = (System.IO.MemoryStream)Clipboard.GetData("PNG");
            if (ms != null)
            {
                //source = BitmapFrame.Create(ms);//これだと取得できない
                source = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
            }
            else if (Clipboard.ContainsImage())
            {
                source = Clipboard.GetImage();
            }
            return source;
        }




        //ファイルドロップ時
        private void Window_Drop(object sender, DragEventArgs e)
        {
            if (e.Data.GetDataPresent(DataFormats.FileDrop) == false) return;
            //ファイルパス取得
            var datas = (string[])e.Data.GetData(DataFormats.FileDrop);
            var paths = datas.ToList();
            paths.Sort();
            MyBitmapOrigin = MakeBitmapSourceGray8FromFile(paths[0]);
            MyImage.Source = MyBitmapOrigin;
            //MyBitmapOrigin = MakeBitmapSourceBgr24FromFile(paths[0]);
            //MyImage.Source = MyBitmapOrigin;
            //MyBitmapOrigin32bit = MakeBitmapSourceBgra32FromFile(paths[0]);
            //MyImage.Source = MyBitmapOrigin32bit;
        }

        //ボタンクリック
        private void MyButton1_Click(object sender, RoutedEventArgs e)
        {
            if (MyBitmapOrigin == null) return;
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / MySliderScale.Value);            
            MyExe(LanczosGray8Ex, MyBitmapOrigin, yoko, tate, (int)MySlider.Value);
        }


        private void MyButton2_Click(object sender, RoutedEventArgs e)
        {
            if (MyBitmapOrigin == null) return;
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(LanczosGray8Ex, MyBitmapOrigin, yoko, tate, (int)MySlider.Value);
        }

        private void MyButton3_Click(object sender, RoutedEventArgs e)
        {
            if (MyBitmapOrigin == null) return;
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / MySliderScale.Value);
            MyExe(LanczosGray8KaiEx, MyBitmapOrigin, yoko, tate, (int)MySlider.Value);
        }

        private void MyButton4_Click(object sender, RoutedEventArgs e)
        {
            if (MyBitmapOrigin == null) return;
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(LanczosGray8KaiEx, MyBitmapOrigin, yoko, tate, (int)MySlider.Value);
        }

        //画像をクリップボードにコピー
        private void MyButtonCopy_Click(object sender, RoutedEventArgs e)
        {
            if (MyBitmapOrigin == null) return;
            ClipboardSetImageWithPng((BitmapSource)MyImage.Source);
        }


        //クリップボードから画像追加
        private void MyButtonPaste_Click(object sender, RoutedEventArgs e)
        {
            BitmapSource img = GetImageFromClipboardWithPNG();
            if (img != null)
            {
                FormatConvertedBitmap bitmap = new(img, PixelFormats.Gray8, null, 0);
                MyBitmapOrigin = bitmap;
                MyImage.Source = bitmap;

                //FormatConvertedBitmap bitmap = new(img, PixelFormats.Bgr24, null, 0);
                //MyBitmapOrigin = bitmap;
                //FormatConvertedBitmap bitmap32 = new(img, PixelFormats.Bgra32, null, 0);
                //MyImage.Source = bitmap32;
            }
        }

        private void MySlider_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            Slider slider = sender as Slider;
            if (e.Delta > 0) slider.Value += slider.SmallChange;
            else slider.Value -= slider.SmallChange;
        }


        private void MyButtonToOrigin_Click(object sender, RoutedEventArgs e)
        {
            MyImage.Source = MyBitmapOrigin;
            //MyImage.Source = MyBitmapOrigin32bit;
        }


        #endregion コピペ
    }
}




テスト

f:id:gogowaten:20210427114207p:plain
輝度値200
f:id:gogowaten:20210427134115p:plain
n=3で1/2
f:id:gogowaten:20210427134312p:plain
輝度値200!
f:id:gogowaten:20210427134501p:plain
n=2でも200!

普通の画像で

f:id:gogowaten:20210427135016p:plain
n=3とn=2
見分けつかないけど、輝度変化なし!


参照距離の違い

f:id:gogowaten:20210427135517p:plain
n=2とn=6
違いがわからん
参照距離は大きいほうがきれいになるはず?だけど、実際にはわからん

別の画像で

f:id:gogowaten:20210427135829p:plain
n=2とn=6
やっぱりわからんw
でも、処理時間は増える

図形画像

f:id:gogowaten:20210423145040j:plain
図形画像

この画像はこちらから引用
Image Resizing for the Web and Email
https://www.cambridgeincolour.com/tutorials/image-resize-for-web.htm

これを1/2

f:id:gogowaten:20210427140308p:plain
n=2とn=6
この画像だと違いがわかるけど、引用元にあるランチョス法の結果と違うんだよねえ、元のほうがきれいに縮小されている

f:id:gogowaten:20210427140609p:plain
n=2からn=6
これで合っているのかなあ

バイリニアとバイキュービックとの比較

f:id:gogowaten:20210427142612p:plain
比較
わからん、こればっかり
バイキュービックのときみたいにカラーだと違いがわかるかもねえ

拡大処理で比較

f:id:gogowaten:20210427144202p:plain
3年前にも使った画像
2倍
f:id:gogowaten:20210427144749p:plain
2倍
拡大処理だとバイリニアがぼやけてしまっているのと、斜めの線がガタガタになっているのがわかる
バイキュービックとランチョスはほとんど同じに見える




感想

前回のバイキュービック法のコードから、重みの計算部分だけ差し替えればいいのかと思っていたら違ったねえ、それでも重み修正以外は同じだったので楽にできた(合っているかどうかは別)
それにしてもバイリニアとの結果がここまで差が出ないとは思わなかった、もっときれいになるかと思っていたから残念、拡大はきれいだけど、拡大は使わないからなあ

次はカラー版




追記2021/05/06

縮小処理が間違っていたので続きの記事
gogowaten.hatenablog.com




関連記事

次回は明日
gogowaten.hatenablog.com

前回のWPF記事は昨日
gogowaten.hatenablog.com

3年前のランチョス
gogowaten.hatenablog.com



リサイズ処理の高速化は3日前
gogowaten.hatenablog.com