午後わてんのブログ

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

WPF、画像の拡大処理を高速化してみた、Parallel.Forとセパラブルで最大22倍速、バイキュービック、グレースケール専用

昨日のバイキュービックでの画像拡大処理を高速化してみた
結果はParallel.Forでの処理の並列化+縦横の処理を別々にするセパラブルフィルタっていう方法で最大22倍速までになった

画像拡大処理の高速化

Test1、Parallel.Forを使って処理を並列化

画像の拡大縮小処理は前後のピクセルの処理の結果を待たなくていい独立した処理なので、ループ処理のForをParallel.Forに書き換えるだけで処理の並列化ができる、これだけでCPUの物理コアのぶんだけ速くなる
(CPUの)レベルを上げて物理(コア数)で殴ればいい
Forが多重になっている場合は一番外側だけ書き換えるのがいい、内側までParallelにすると逆に遅くなるはず




Test2、4x4の重み計算をまとめる

参照範囲の4x4ピクセルの重み計算をまとめたら速くなるんじゃないかと

f:id:gogowaten:20210424152741p:plain
まとめて計算?
左が通常、右をこうしてみたけど、失敗だった
全然まとまっていない
速くなるどころか遅くなってしまった




Test3、画像範囲を広げる

f:id:gogowaten:20210424160132p:plain
はみ出しチェック
画像周縁部の処理では参照範囲が、画像の範囲外になることがあるから、そのときは一番近くのピクセルを対象にするんだけど、このチェックがめんどくさい、だったら最大参照距離の2ピクセルぶんを元画像の外側に付け足した画像を作って、それで処理すればチェックしなくていいから速くなるんじゃないか?
f:id:gogowaten:20210424160709p:plain
元画像と拡張した画像範囲
3x3の画像なら7x7に拡張
拡張は上下左右に2ピクセルなので、サイズは縦横ともに+4ピクセルになる

f:id:gogowaten:20210424160938p:plain
Indexの計算
サイズが変わるからIndexもずれるので、参照するときは修正する必要がある
拡張範囲は実範囲より上側が2行多い
1行分のIndexは0~6の7(stride)なので
2行分のIndexは2Stride=27=14
これに加えて左側に2ピクセル足せばいい、14+2=16
要は2行+2を足せばいいことになる

元画像3x3の(1,1)の場合は
(1,1)は1行と1なので、2行+2に足すと3行+3
3行+3は3*7+3=24
(1,1)はIndex24を見ればいい

f:id:gogowaten:20210424163436p:plain
Indexの計算
はみ出しチェックが必要なくなったので速くなるはず

拡張範囲の値

f:id:gogowaten:20210424161456p:plain
拡張範囲の値
外周の値をそのまま延ばす感じ

同じ値の行

f:id:gogowaten:20210424163957p:plain
同じ値の行
上の2行は元画像でいう最初の行と全くおなじになっている
下の2行も同じように最下段と同じになっている

拡張範囲の作成

f:id:gogowaten:20210424164440p:plain
拡張範囲の作成
上下2行以外の中間層の値を入れる、上下2行は中間層の上下と同じ値なのを利用して、そのままコピーして作成

こんな感じで結構手間がかかるわけだけど、この方法はあまり速くならなくて、1~2割の改善にとどまった




Test4、縦横の処理を分けるセパラブル

この方法は
www.rainorshine.asia こちらと
qiita.com こちらより
縦と横の処理を別にして順番に処理する方法、セパラブルっていうらしい
この方法はすごくて4倍前後速くなる
重み計算の回数は
10x10ピクセルを10倍拡大で100x100にするときに処理するピクセル数は
通常だと100 * 100=10000
セパラブルだと横処理で10 * 100=1000、その後の縦処理で100*100=10000、合計11000

1ピクセルあたりの重み計算回数は
通常は縦横4x4なので16回、セパラブルは縦か横だけなので4回

これをピクセル数に掛け算したのが全体の計算回数
通常、10000 * 16=160000回
セパ、11000 * 4=44000回
ってことでセパラブルのほうが計算回数が少ない
160000/44000=3.6363636
約3.6倍速くなる、実際にはもっと速くなるのは重み計算自体が、他のところの計算より時間がかかっているってことなのかも、なんにしてもこんな方法を思いつくのがすごい




Test5、Test2+Test3

Test2、4x4の重み計算をまとめると、Test3の画像範囲を広げる、この両方を使ってみたけど遅くなった、失敗




Test6、重み計算を予めしておく、テーブル

大失敗だったTest2の改善版みたいな感じで、Test2では1ピクセルごとに計算していた重み計算を、全部まとめて1回にしたもの

f:id:gogowaten:20210424191949p:plain
テーブル
0.1刻みでの重み計算をすべて事前にしておく
f:id:gogowaten:20210424192843p:plain
テーブルの中身
4x4の配列が11x11の配列に入った形になっている
これを使った結果1.7倍速くなった、かなりの改善だけど10倍に拡大とかすると、0.1刻みっていう粗さのせいで画質がイマイチになる




Test7、Test3+Test6

拡張範囲+テーブルの組み合わせ
結果は1.8倍速くなった
単独ではそれぞれ1.2倍と1.7倍だったから1.2*1.7=2.04、イマイチな伸び?




Test8、Test1+Test4

今回最速の組み合わせは
Parallel+セパラブル、これで最大22倍まで速くなった
単独では5倍と4倍だったので、5*4=20で順当な伸び、素晴らしい




Test9、Test6+Test8

Parallel+セパラブル+テーブルの組み合わせ
20倍速になったParallel+セパラブルに1.7倍速のテーブルを組み合わせれば更に速くなるんじゃないかと思ったけど、実際には遅くなって最大で13倍速だった
これはセパラブルとテーブルの組み合わせは相性が良くないからだと思う
普通のときのテーブルは縦横の重みの掛け算も済ましておけるんだけど、セパラブルは縦横別計算だからこれができないので、テーブルが活かせない感じだった


テストアプリ

f:id:gogowaten:20210424200042p:plain
テストアプリ
画像ファイルドロップ、クリップボードから貼り付けで画像表示
画像表示しないでTestボタン押すとエラーで止まる
グレースケール専用
カラー画像はグレースケールに変換されて表示
倍率は1~10倍拡大、1刻み
-1.00のところはバイキュービックに指定する定数、-0.5~-1.0くらいがきれいになる、小さいとシャープ、大きいくするとぼやける(ソフト)
各Testボタン押すと処理開始、処理が終わるまで操作不能、処理が終わると左上に処理時間表示


ダウンロード先

github.com
ダウンロードはここの20210423_バイキュービック処理速度.zip




作成動作環境




コード

MainWindow.xaml

<Window x:Class="_20210423_バイキュービック処理速度.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:_20210423_バイキュービック処理速度"
        mc:Ignorable="d"
        Title="MainWindow" Height="600" Width="634"
        AllowDrop="True" Drop="Window_Drop">
  <Window.Resources>
    <Style TargetType="Button">
      <Setter Property="Margin" Value="2,4,2,0"/>
    </Style>
  </Window.Resources>
  <Grid>
    <DockPanel UseLayoutRounding="True">
      <StatusBar DockPanel.Dock="Top">
        <TextBlock x:Name="MyTextBlockTime" Text="time"/>
      </StatusBar>
      <StackPanel DockPanel.Dock="Right" Background="White">
        <Slider x:Name="MySliderScale" Minimum="1" Maximum="10" SmallChange="1" TickFrequency="1" IsSnapToTickEnabled="True"
                Value="2" Width="80" HorizontalAlignment="Center" MouseWheel="MySlider_MouseWheel">
          <Slider.LayoutTransform>
            <RotateTransform Angle="270"/>
          </Slider.LayoutTransform>
        </Slider>
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
          <TextBlock Text="倍率 = "/>
          <TextBlock Text="{Binding ElementName=MySliderScale, Path=Value}"/>
        </StackPanel>
        <Button x:Name="MyButton0" Content="Test0" Click="MyButton0_Click"/>
        <Button x:Name="MyButton1" Content="Test1" Click="MyButton1_Click"/>
        <Button x:Name="MyButton2" Content="Test2" Click="MyButton2_Click"/>
        <Button x:Name="MyButton3" Content="Test3" Click="MyButton3_Click"/>
        <Button x:Name="MyButton4" Content="Test4" Click="MyButton4_Click"/>
        <Button x:Name="MyButton5" Content="Test5" Click="MyButton5_Click"/>
        <Button x:Name="MyButton6" Content="Test6" Click="MyButton6_Click"/>
        <Button x:Name="MyButton7" Content="Test7" Click="MyButton7_Click"/>
        <Button x:Name="MyButton8" Content="Test8" Click="MyButton8_Click"/>
        <Button x:Name="MyButton9" Content="Test9" Click="MyButton9_Click"/>
        <Button x:Name="MyButtonToOrigin" Content="戻す" Click="MyButtonToOrigin_Click"/>
        <Slider x:Name="MySlider" Minimum="-3.0" Maximum="3.0" SmallChange="0.05" TickFrequency="0.1" IsSnapToTickEnabled="True"
                Value="-1.0" Width="80" HorizontalAlignment="Center" MouseWheel="MySlider_MouseWheel">
          <Slider.LayoutTransform>
            <RotateTransform Angle="270"/>
          </Slider.LayoutTransform>
        </Slider>
        <TextBlock Text="{Binding ElementName=MySlider, Path=Value, StringFormat=0.00}"
                   HorizontalAlignment="Center"/>
        <Button x:Name="MyButtonCopy" Content="コピー" Click="MyButtonCopy_Click" Margin="10"/>
        <Button x:Name="MyButtonPaste" Content="ペースト" Click="MyButtonPaste_Click" Margin="10"/>
      </StackPanel>
      <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <Image x:Name="MyImage" Stretch="None"/>
      </ScrollViewer>
    </DockPanel>
  </Grid>
</Window>




MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;

using System.Diagnostics;



//Bicubic(バイキュービック法)~1.基本編~ | Rain or Shine
//https://www.rainorshine.asia/2013/04/03/post2351.html
//Bicubic(バイキュービック法)~3.さらに高速化編~ | Rain or Shine
//https://www.rainorshine.asia/2013/12/13/post2497.html
//    Bicubic interpolation - Wikipedia
//https://en.wikipedia.org/wiki/Bicubic_interpolation
//    画像の拡大「Bicubic法」: koujinz blog
//http://koujinz.cocolog-nifty.com/blog/2009/05/bicubic-a97c.html
//画像リサイズ処理のうんちく - Qiita
//https://qiita.com/yoya/items/95c37e02762431b1abf0#%E3%82%BB%E3%83%91%E3%83%A9%E3%83%96%E3%83%AB%E3%83%95%E3%82%A3%E3%83%AB%E3%82%BF



namespace _20210423_バイキュービック処理速度
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        private BitmapSource MyBitmapOrigin;
        public MainWindow()
        {
            InitializeComponent();
#if DEBUG
            this.Top = 0;
            this.Left = 0;
#endif
            //List<double> vs = new();
            //for (int i = 0; i < 10; i++)
            //{
            //    vs.Add(i / 10.0);
            //}
        }


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

        }

        /// <summary>
        /// バイキュービックで重み計算
        /// </summary>
        /// <param name="d">距離</param>
        /// <param name="a">定数、-1.0 ~ -0.5 が丁度いい</param>
        /// <returns></returns>
        private static double GetWeightCubic(double d, double a = -1.0)
        {
            return d switch
            {
                2 => 0,
                <= 1 => ((a + 2) * (d * d * d)) - ((a + 3) * (d * d)) + 1,
                < 2 => (a * (d * d * d)) - (5 * a * (d * d)) + (8 * a * d) - (4 * a),
                _ => 0
            };
        }



        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// セパラブル+Parallel+テーブル
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、基準は-1.0、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test9(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];
            double[] xResult = new double[sourceHeight * scaledStride];

            //重みのテーブル取得
            Dictionary<int, double[]> table = GetWeightTable(a);

            //横方だけを処理
            _ = Parallel.For(0, sourceHeight, y =>
              {
                  for (int x = 0; x < yoko; x++)
                  {
                      //参照点
                      double rx = (x + 0.5) * yokoScale;
                      //参照点四捨五入で基準
                      int xKijun = (int)(rx + 0.5);
                      //テーブルのkey作成、基準の距離を10倍して四捨五入
                      double xDis = Math.Abs(rx - (xKijun + 0.5));
                      var xKey = (int)((xDis * 10.0) + 0.5);

                      double vv = 0;
                      //参照範囲は基準から左へ2、右へ1の範囲
                      for (int xx = -2; xx <= 1; xx++)
                      {
                          int xc = xKijun + xx;
                          xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                          byte value = pixels[y * stride + xc];
                          vv += value * table[xKey][xx + 2];
                      }
                      xResult[y * scaledStride + x] = vv;
                  }
              });

            //縦方向も処理
            _ = Parallel.For(0, tate, y =>
              {
                  for (int x = 0; x < yoko; x++)
                  {
                      //参照点
                      double ry = (y + 0.5) * tateScale;
                      //参照点四捨五入で基準
                      int yKijun = (int)(ry + 0.5);
                      //テーブルのkey作成、基準の距離を10倍して四捨五入
                      double yDis = Math.Abs(ry - (yKijun + 0.5));
                      var yKey = (int)((yDis * 10.0) + 0.5);

                      double vv = 0;
                      //参照範囲は基準から左へ2、右へ1の範囲
                      for (int yy = -2; yy <= 1; yy++)
                      {
                          int yc = yKijun + yy;
                          //マイナス座標や画像サイズを超えていたら、収まるように修正
                          yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                          double value = xResult[yc * scaledStride + x];
                          vv += value * table[yKey][yy + 2];
                      }
                      //0~255の範囲を超えることがあるので、修正
                      vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                      resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                  }
              });

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;

            //重みのテーブル作成、0.1刻み           
            Dictionary<int, double[]> GetWeightTable(double a)
            {
                Dictionary<int, double[]> table = new();
                for (int i = 0; i < 11; i++)
                {
                    double d = i / 10.0;
                    table.Add(i, new double[] {
                    GetWeightCubic(2 - d, a),
                    GetWeightCubic(1 - d, a),
                    GetWeightCubic(d, a),
                    GetWeightCubic(1 + d, a) });
                }
                return table;
            }
        }



        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// セパラブル+Parallel
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、基準は-1.0、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test8(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];
            double[] xResult = new double[sourceHeight * scaledStride];

            //横方だけを処理
            Parallel.For(0, sourceHeight, y =>
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * yokoScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);

                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int xx = -2; xx <= 1; xx++)
                    {
                        //+0.5しているのは中心座標で計算するため
                        double dx = Math.Abs(rx - (xx + xKijun + 0.5));//距離
                        double xw = GetWeightCubic(dx, a);//重み
                        int xc = xKijun + xx;
                        xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                        byte value = pixels[y * stride + xc];
                        vv += value * xw;
                    }
                    xResult[y * scaledStride + x] = vv;
                }
            });

            //縦方向も処理
            Parallel.For(0, tate, y =>
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double ry = (y + 0.5) * tateScale;
                    //参照点四捨五入で基準
                    int yKijun = (int)(ry + 0.5);

                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)
                    {
                        double dy = Math.Abs(ry - (yy + yKijun + 0.5));
                        double yw = GetWeightCubic(dy, a);
                        int yc = yKijun + yy;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        double value = xResult[yc * scaledStride + x];
                        vv += value * yw;
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                }
            });

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;
        }





        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// 拡張範囲+テーブル
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test7(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];

            //拡張範囲、元のサイズから左右上下に2ピクセル拡張
            byte[] exPixels = new byte[(sourceHeight + 4) * (stride + 4)];
            int exStride = stride + 4;
            int exWidth = sourceWidth + 4;
            int exHeight = sourceHeight + 4;

            //中段
            int count = 0;
            for (int i = exWidth + exWidth + 2; i < exPixels.Length - exStride - exStride; i += exWidth)
            {
                exPixels[i - 2] = pixels[count];//左端
                exPixels[i - 1] = pixels[count];//左端の1個右
                for (int j = 0; j < sourceWidth; j++)
                {
                    exPixels[i + j] = pixels[count];//中間
                    count++;
                }
                exPixels[i + sourceWidth] = pixels[count - 1];//右端1個左
                exPixels[i + sourceWidth + 1] = pixels[count - 1];//右端
            }
            //最上段とその1段下と最下段とその1段上の行
            int endRow = (exStride * exHeight) - exStride;
            for (int i = 0; i < exStride; i++)
            {
                exPixels[i] = exPixels[i + exStride + exStride];//最上段
                exPixels[i + exStride] = exPixels[i + exStride + exStride];//最上段の1段下
                exPixels[endRow + i - exStride] = exPixels[i + endRow - exStride - exStride];
                exPixels[endRow + i] = exPixels[i + endRow - exStride - exStride];//最下段
            }

            //重みのテーブル取得
            double[,][,] table = GetWeightTable(a);

            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * yokoScale;
                    double ry = (y + 0.5) * tateScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);
                    int yKijun = (int)(ry + 0.5);

                    //テーブルのkey作成、基準の距離を10倍して四捨五入
                    double xDis = Math.Abs(rx - (xKijun + 0.5));
                    var xKey = (int)((xDis * 10.0) + 0.5);
                    double yDis = Math.Abs(ry - (yKijun + 0.5));
                    var yKey = (int)((yDis * 10.0) + 0.5);

                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)//
                    {
                        int yFix = ((yKijun + yy) * exStride) + exStride + exStride + 2;
                        for (int xx = -2; xx <= 1; xx++)
                        {
                            byte value = exPixels[yFix + xKijun + xx];
                            vv += value * table[xKey, yKey][xx + 2, yy + 2];
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                }
            };

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;


            //重みのテーブル作成、0.1刻み           
            double[,][,] GetWeightTable(double a)
            {
                Dictionary<int, double[]> tt = new();
                for (int i = 0; i < 11; i++)
                {
                    double d = i / 10.0;
                    tt.Add(i, new double[] {
                    GetWeightCubic(2 - d, a),
                    GetWeightCubic(1 - d, a),
                    GetWeightCubic(d, a),
                    GetWeightCubic(1 + d, a) });
                }

                double[,][,] table = new double[11, 11][,];
                for (int y = 0; y < 11; y++)
                {
                    for (int x = 0; x < 11; x++)
                    {
                        double[,] tableB = new double[4, 4];
                        for (int iy = 0; iy < 4; iy++)
                        {
                            for (int ix = 0; ix < 4; ix++)
                            {
                                tableB[ix, iy] = tt[y][iy] * tt[x][ix];
                            }
                        }
                        table[x, y] = tableB;
                    }
                }
                return table;
            }
        }





        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// 重みのテーブル作成して使用
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定するのがいい、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test6(BitmapSource source, int width, int height, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int sourceStride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] sourcePixels = new byte[sourceHeight * sourceStride];
            source.CopyPixels(sourcePixels, sourceStride, 0);

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

            //重みのテーブル取得
            double[,][,] table = GetWeightTable(a);

            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);
                    //テーブルのkey作成、基準の距離を10倍して四捨五入
                    double xDis = Math.Abs(rx - (xKijun + 0.5));
                    var xKey = (int)((xDis * 10.0) + 0.5);
                    double yDis = Math.Abs(ry - (yKijun + 0.5));
                    var yKey = (int)((yDis * 10.0) + 0.5);

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

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

            //重みのテーブル作成、0.1刻み           
            double[,][,] GetWeightTable(double a)
            {
                Dictionary<int, double[]> tt = new();
                for (int i = 0; i < 11; i++)
                {
                    double d = i / 10.0;
                    tt.Add(i, new double[] {
                    GetWeightCubic(2 - d, a),
                    GetWeightCubic(1 - d, a),
                    GetWeightCubic(d, a),
                    GetWeightCubic(1 + d, a) });
                }

                double[,][,] table = new double[11, 11][,];
                for (int y = 0; y < 11; y++)
                {
                    for (int x = 0; x < 11; x++)
                    {
                        double[,] tableB = new double[4, 4];
                        for (int iy = 0; iy < 4; iy++)
                        {
                            for (int ix = 0; ix < 4; ix++)
                            {
                                tableB[ix, iy] = tt[y][iy] * tt[x][ix];
                            }
                        }
                        table[x, y] = tableB;
                    }
                }
                return table;
            }

        }




        //
        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// 4x4の重みまとめて計算+拡張範囲
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test5(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];

            //拡張範囲、もとのサイズから左右上下に2ピクセル拡張
            byte[] exPixels = new byte[(sourceHeight + 4) * (stride + 4)];
            int exStride = stride + 4;
            int exWidth = sourceWidth + 4;
            int exHeight = sourceHeight + 4;

            //中段
            int count = 0;
            for (int i = exWidth + exWidth + 2; i < exPixels.Length - exStride - exStride; i += exWidth)
            {
                exPixels[i - 2] = pixels[count];//左端
                exPixels[i - 1] = pixels[count];//左端の1個右
                for (int j = 0; j < sourceWidth; j++)
                {
                    exPixels[i + j] = pixels[count];//中間
                    count++;
                }
                exPixels[i + sourceWidth] = pixels[count - 1];//右端1個左
                exPixels[i + sourceWidth + 1] = pixels[count - 1];//右端
            }
            //最上段とその1段下と最下段とその1段上の行
            int endRow = (exStride * exHeight) - exStride;
            for (int i = 0; i < exStride; i++)
            {
                exPixels[i] = exPixels[i + exStride + exStride];//最上段
                exPixels[i + exStride] = exPixels[i + exStride + exStride];//最上段の1段下
                exPixels[endRow + i - exStride] = exPixels[i + endRow - exStride - exStride];
                exPixels[endRow + i] = exPixels[i + endRow - exStride - exStride];//最下段
            }

            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * yokoScale;
                    double ry = (y + 0.5) * tateScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);
                    int yKijun = (int)(ry + 0.5);
                    //4x4の重み取得
                    double[,] ws = Get4x4Weight(rx, ry);
                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    int topLeft = ((yKijun - 2) * exStride) + exStride + exStride + 2 + xKijun - 2;
                    vv += exPixels[topLeft] * ws[0, 0];
                    vv += exPixels[topLeft + 1] * ws[1, 0];
                    vv += exPixels[topLeft + 2] * ws[2, 0];
                    vv += exPixels[topLeft + 3] * ws[3, 0];
                    topLeft += exStride;//改行
                    vv += exPixels[topLeft] * ws[0, 1];
                    vv += exPixels[topLeft + 1] * ws[1, 1];
                    vv += exPixels[topLeft + 2] * ws[2, 1];
                    vv += exPixels[topLeft + 3] * ws[3, 1];
                    topLeft += exStride;
                    vv += exPixels[topLeft] * ws[0, 2];
                    vv += exPixels[topLeft + 1] * ws[1, 2];
                    vv += exPixels[topLeft + 2] * ws[2, 2];
                    vv += exPixels[topLeft + 3] * ws[3, 2];
                    topLeft += exStride;
                    vv += exPixels[topLeft] * ws[0, 3];
                    vv += exPixels[topLeft + 1] * ws[1, 3];
                    vv += exPixels[topLeft + 2] * ws[2, 3];
                    vv += exPixels[topLeft + 3] * ws[3, 3];

                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                }
            };

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;

            //4x4個すべての重み計算
            double[,] Get4x4Weight(double rx, double ry)
            {
                double sx = rx - (int)rx;
                double sy = ry - (int)ry;
                //double sx = rx % 1;
                //double sy = ry % 1;
                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[] {
                    GetWeightCubic(2 - dx, a),
                    GetWeightCubic(1 - dx, a),
                    GetWeightCubic(dx, a),
                    GetWeightCubic(1 + dx, a) };
                double[] yw = new double[] {
                    GetWeightCubic(2 - dy, a),
                    GetWeightCubic(1 - dy, a),
                    GetWeightCubic(dy, a),
                    GetWeightCubic(1 + dy, a) };

                double[,] ws = new double[4, 4];
                for (int yy = 0; yy < 4; yy++)
                {
                    ws[0, yy] = xw[0] * yw[yy];
                    ws[1, yy] = xw[1] * yw[yy];
                    ws[2, yy] = xw[2] * yw[yy];
                    ws[3, yy] = xw[3] * yw[yy];
                }
                return ws;
            }
        }



        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// セパラブル
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、基準は-1.0、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test4(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];
            double[] xResult = new double[sourceHeight * scaledStride];

            //横方だけを処理
            for (int y = 0; y < sourceHeight; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * yokoScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);

                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int xx = -2; xx <= 1; xx++)
                    {
                        //+0.5しているのは中心座標で計算するため
                        double dx = Math.Abs(rx - (xx + xKijun + 0.5));//距離
                        double xw = GetWeightCubic(dx, a);//重み
                        int xc = xKijun + xx;
                        xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                        byte value = pixels[y * stride + xc];
                        vv += value * xw;
                    }
                    xResult[y * scaledStride + x] = vv;
                }
            };
            //縦方向も処理
            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double ry = (y + 0.5) * tateScale;
                    //参照点四捨五入で基準
                    int yKijun = (int)(ry + 0.5);

                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)
                    {
                        double dy = Math.Abs(ry - (yy + yKijun + 0.5));
                        double yw = GetWeightCubic(dy, a);
                        int yc = yKijun + yy;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        double value = xResult[yc * scaledStride + x];
                        vv += value * yw;
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                }
            }

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;
        }




        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// 拡張範囲
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test3(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];

            //拡張範囲、元のサイズから左右上下に2ピクセル拡張
            byte[] exPixels = new byte[(sourceHeight + 4) * (stride + 4)];
            int exStride = stride + 4;
            int exWidth = sourceWidth + 4;
            int exHeight = sourceHeight + 4;

            //中段
            int count = 0;
            for (int i = exWidth + exWidth + 2; i < exPixels.Length - exStride - exStride; i += exWidth)
            {
                exPixels[i - 2] = pixels[count];//左端
                exPixels[i - 1] = pixels[count];//左端の1個右
                for (int j = 0; j < sourceWidth; j++)
                {
                    exPixels[i + j] = pixels[count];//中間
                    count++;
                }
                exPixels[i + sourceWidth] = pixels[count - 1];//右端1個左
                exPixels[i + sourceWidth + 1] = pixels[count - 1];//右端
            }
            //最上段とその1段下と最下段とその1段上の行
            int endRow = (exStride * exHeight) - exStride;
            for (int i = 0; i < exStride; i++)
            {
                exPixels[i] = exPixels[i + exStride + exStride];//最上段
                exPixels[i + exStride] = exPixels[i + exStride + exStride];//最上段の1段下
                exPixels[endRow + i - exStride] = exPixels[i + endRow - exStride - exStride];
                exPixels[endRow + i] = exPixels[i + endRow - exStride - exStride];//最下段
            }

            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * yokoScale;
                    double ry = (y + 0.5) * tateScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);
                    int yKijun = (int)(ry + 0.5);

                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)//
                    {
                        int yFix = ((yKijun + yy) * exStride) + exStride + exStride + 2;
                        //+0.5しているのは中心座標で計算するため
                        double dy = Math.Abs(ry - (yy + yKijun + 0.5));//距離
                        double yw = GetWeightCubic(dy, a);//重み
                        for (int xx = -2; xx <= 1; xx++)
                        {
                            double dx = Math.Abs(rx - (xx + xKijun + 0.5));
                            double xw = GetWeightCubic(dx, a);
                            byte value = exPixels[yFix + xKijun + xx];
                            vv += value * yw * xw;
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                }
            };

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;
        }



        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// 4x4の重みまとめて計算
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicGray8Test2(BitmapSource source, int yoko, int tate, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int stride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[sourceHeight * stride];
            source.CopyPixels(pixels, stride, 0);

            //変換後の画像の画素値の配列用
            double yokoScale = (double)sourceWidth / yoko;//横倍率
            double tateScale = (double)sourceHeight / tate;
            int scaledStride = (yoko * source.Format.BitsPerPixel + 7) / 8;
            byte[] resultPixels = new byte[tate * scaledStride];

            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点
                    double rx = (x + 0.5) * yokoScale;
                    double ry = (y + 0.5) * tateScale;
                    //参照点四捨五入で基準
                    int xKijun = (int)(rx + 0.5);
                    int yKijun = (int)(ry + 0.5);
                    //4x4の重み取得
                    double[,] ws = Get4x4Weight(rx, ry);
                    double vv = 0;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)//
                    {
                        int yc = yKijun + yy;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        for (int xx = -2; xx <= 1; xx++)
                        {
                            int xc = xKijun + xx;
                            xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                            byte value = pixels[yc * stride + xc];
                            vv += value * ws[xx + 2, yy + 2];
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    resultPixels[y * scaledStride + x] = (byte)(vv + 0.5);
                }
            };

            BitmapSource bitmap = BitmapSource.Create(yoko, tate, 96, 96, source.Format, null, resultPixels, scaledStride);
            return bitmap;

            //4x4個すべての重み計算
            double[,] Get4x4Weight(double rx, double ry)
            {
                double sx = rx - (int)rx;
                double sy = ry - (int)ry;
                //double sx = rx % 1;
                //double sy = ry % 1;
                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[] {
                    GetWeightCubic(2 - dx, a),
                    GetWeightCubic(1 - dx, a),
                    GetWeightCubic(dx, a),
                    GetWeightCubic(1 + dx, a) };
                double[] yw = new double[] {
                    GetWeightCubic(2 - dy, a),
                    GetWeightCubic(1 - dy, a),
                    GetWeightCubic(dy, a),
                    GetWeightCubic(1 + dy, a) };

                double[,] ws = new double[4, 4];
                for (int yy = 0; yy < 4; yy++)
                {
                    ws[0, yy] = xw[0] * yw[yy];
                    ws[1, yy] = xw[1] * yw[yy];
                    ws[2, yy] = xw[2] * yw[yy];
                    ws[3, yy] = xw[3] * yw[yy];
                }
                return ws;
            }
        }





        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// parallel.for
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定するのがいい、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private static BitmapSource BicubicGray8Test1(BitmapSource source, int width, int height, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int sourceStride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] sourcePixels = new byte[sourceHeight * sourceStride];
            source.CopyPixels(sourcePixels, sourceStride, 0);

            //変換後の画像の画素値の配列用
            double widthScale = (double)sourceWidth / width;//横倍率
            double heightScale = (double)sourceHeight / height;
            int stride = (width * source.Format.BitsPerPixel + 7) / 8;
            byte[] pixels = new byte[height * stride];
            Parallel.For(0, 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;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)//
                    {
                        //+0.5しているのは中心座標で計算するため
                        double dy = Math.Abs(ry - (yy + yKijun + 0.5));//距離
                        double yw = GetWeightCubic(dy, a);//重み
                        int yc = yKijun + yy;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        for (int xx = -2; xx <= 1; xx++)
                        {
                            double dx = Math.Abs(rx - (xx + xKijun + 0.5));
                            double xw = GetWeightCubic(dx, a);
                            int xc = xKijun + xx;
                            xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                            byte value = sourcePixels[yc * sourceStride + xc];
                            vv += value * yw * xw;
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    pixels[y * stride + x] = (byte)(vv + 0.5);
                }
            });


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




        /// <summary>
        /// 画像の縮小、バイキュービック法で補完、PixelFormats.Gray8専用)
        /// a指定版
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定するのがいい、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private static BitmapSource BicubicGray8Test0(BitmapSource source, int width, int height, double a = -1.0)
        {
            //元画像の画素値の配列作成
            int sourceWidth = source.PixelWidth;
            int sourceHeight = source.PixelHeight;
            int sourceStride = (sourceWidth * source.Format.BitsPerPixel + 7) / 8;
            byte[] sourcePixels = new byte[sourceHeight * sourceStride];
            source.CopyPixels(sourcePixels, sourceStride, 0);

            //変換後の画像の画素値の配列用
            double widthScale = (double)sourceWidth / width;//横倍率
            double heightScale = (double)sourceHeight / height;
            int stride = (width * source.Format.BitsPerPixel + 7) / 8;
            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;
                    //参照範囲は基準から左へ2、右へ1の範囲
                    for (int yy = -2; yy <= 1; yy++)//
                    {
                        //+0.5しているのは中心座標で計算するため
                        double dy = Math.Abs(ry - (yy + yKijun + 0.5));//距離
                        double yw = GetWeightCubic(dy, a);//重み
                        int yc = yKijun + yy;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        for (int xx = -2; xx <= 1; xx++)
                        {
                            double dx = Math.Abs(rx - (xx + xKijun + 0.5));
                            double xw = GetWeightCubic(dx, a);
                            int xc = xKijun + xx;
                            xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                            byte value = sourcePixels[yc * sourceStride + xc];
                            vv += value * yw * xw;
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    vv = vv < 0 ? 0 : vv > 255 ? 255 : vv;
                    pixels[y * stride + x] = (byte)(vv + 0.5);
                }
            };

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


        /// <summary>
        /// 画像ファイルパスからPixelFormats.Gray8のBitmapSource作成
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="dpiX"></param>
        /// <param name="dpiY"></param>
        /// <returns></returns>
        private static 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 static 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 static 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;
        }

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


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

        private void MyButton3_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test3, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }

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

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


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

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


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


        #endregion コピペ

        private void MyButton0_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test0, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }

        private void MyButton5_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test5, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }

        private void MyButton6_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test6, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }

        private void MyButton7_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test7, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }

        private void MyButton8_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test8, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }

        private void MyButton9_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value);
            MyExe(BicubicGray8Test9, MyBitmapOrigin, yoko, tate, MySlider.Value);
        }
    }
}




テスト

使った画像

f:id:gogowaten:20210424200647j:plain
1088x816のjpeg写真画像マツバウンランの花

f:id:gogowaten:20210424200749p:plain
ファイルドロップで表示したところ

f:id:gogowaten:20210424200902p:plain
倍率2でTest0を実行したところ
左上に処理時間と実行した関数名表示
これで倍率2,5,10を各Testでの処理時間計測

テスト結果

f:id:gogowaten:20210424201628p:plain
処理時間
これをグラフにして
f:id:gogowaten:20210424201646p:plain
処理時間グラフ


どれだけ速くなったのか

f:id:gogowaten:20210424202328p:plain
何倍速?
これもグラフにして
f:id:gogowaten:20210424202356p:plain
何倍速?グラフ

単独で一番速くなったのはTest1のParallel、CPUの物理コア数が多ければもっと速くなるはず
Ryzen 5 2400Gは物理コア4個、論理コア8個、これで5倍速くなった、4倍じゃなくて5倍なのは論理コアも効き目があるってことかな
戦いは(物理コア)数だよ兄貴!

次に速かったのはTest4のセパラブルで4倍速くなった、素晴らしい

次いでテーブルが1.7倍、拡張範囲が1.2倍となった
完全に失敗だったのはTest2

最速+最速=最速
最大22倍速の結果を出したのはセパラブルをParallel化(マルチスレッド化)したTest8、12秒かかる処理が0.5秒とかで終わるからぜんぜん違うんだよね、素晴らしい
これにテーブルを組み合わせれば速くなるかと思ったら遅くなったのが最後のTest9、残念、セパラブルとテーブルの相性が良くない、書き方が良くないのかもしれないけどねえ、あと今回のテーブルは0.1刻みと粗かったせいか10倍拡大処理だと

f:id:gogowaten:20210424204537p:plain
10倍拡大時の比較
下がテーブル使用、上の通常のものより階調が荒くガタガタになってしまう
とはいっても、5倍拡大くらいなら
f:id:gogowaten:20210424204927p:plain
5倍拡大時の比較
見た感じでは差がない



感想

使うとしたら、お手軽にCPUの力を使うTest1のParallelと、少し手間がかかるけどTest4のセパラブル、この2つ
テーブルはセパラブルとの相性が良くないからパス
拡張範囲での処理は準備がめんどくさい割に1.2倍しか速くならなかったけれど、周縁部の処理が楽になるのがいい。周縁部の処理にはいくつか方法があるみたいなので、それを試すときに使えるかも




関連記事

次回は明後日
gogowaten.hatenablog.com

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

3年前
gogowaten.hatenablog.com
2重Forを2つともParallelにすると遅くなるんじゃなくてPCの動作がカクカクになるだった、どちらにしても意味ない外側1個で十分

2年前
gogowaten.hatenablog.com

去年
gogowaten.hatenablog.com