午後わてんのブログ

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

C#、WPF、バイキュービック補完法での画像の拡大縮小変換、24bit(普通のカラー)と32bit(半透明画像)対応版

前々回のグレースケール専用バイキュービックのカラー版

コード

24bitカラーまで対応版

/// <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.Bgr24専用)
/// 通常版
/// </summary>
/// <param name="source">PixelFormats.Bgr24のBitmap</param>
/// <param name="width">変換後の横ピクセル数を指定</param>
/// <param name="height">変換後の縦ピクセル数を指定</param>
/// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
/// <returns></returns>
private BitmapSource BicubicBgr24(BitmapSource source, int width, int height, double a = -1.0)
{
    //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 bv = 0, gv = 0, rv = 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;
                    double weight = yw * xw;
                    int pp = (yc * sourceStride) + (xc * pByte);
                    bv += sourcePixels[pp] * weight;
                    gv += sourcePixels[pp + 1] * weight;
                    rv += sourcePixels[pp + 2] * weight;
                }
            }
            //0~255の範囲を超えることがあるので、修正
            bv = bv < 0 ? 0 : bv > 255 ? 255 : bv;
            gv = gv < 0 ? 0 : gv > 255 ? 255 : gv;
            rv = rv < 0 ? 0 : rv > 255 ? 255 : rv;
            int ap = (y * stride) + (x * pByte);
            pixels[ap] = (byte)(bv + 0.5);
            pixels[ap + 1] = (byte)(gv + 0.5);
            pixels[ap + 2] = (byte)(rv + 0.5);
        }
    };

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

昨日のグレースケール専用を普通に書き換えただけ




32bitカラーまで対応版

/// <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.Bgra32専用)
/// 通常版
/// </summary>
/// <param name="source">PixelFormats.Bgra32のBitmap</param>
/// <param name="width">変換後の横ピクセル数を指定</param>
/// <param name="height">変換後の縦ピクセル数を指定</param>
/// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
/// <returns></returns>
private BitmapSource BicubicBgra32(BitmapSource source, int width, int height, double a = -1.0)
{
    //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];

    int pp;
    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 bv = 0, gv = 0, rv = 0, av = 0;
            double alphaFix = 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;
                    double weight = yw * xw;
                    pp = (yc * sourceStride) + (xc * pByte);
                    //完全透明ピクセル(a=0)だった場合はRGBは計算しないで
                    //重みだけ足し算して後で使う
                    if (sourcePixels[pp + 3] == 0)
                    {
                        alphaFix += weight;
                        continue;
                    }
                    bv += sourcePixels[pp] * weight;
                    gv += sourcePixels[pp + 1] * weight;
                    rv += sourcePixels[pp + 2] * weight;
                    av += sourcePixels[pp + 3] * weight;
                }
            }

            //                 C#、WPF、バイリニア法での画像の拡大縮小変換、半透明画像(32bit画像)対応版 - 午後わてんのブログ
            //https://gogowaten.hatenablog.com/entry/2021/04/17/151803#32bit%E3%81%A824bit%E3%81%AF%E9%81%95%E3%81%A3%E3%81%9F

            //完全透明ピクセルによるRGB値の修正
            //16ピクセルすべて完全透明だった場合は計算しない0のまま
            if (alphaFix == 1) continue;
            //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する
            double rgbFix = 1 / (1 - alphaFix);
            bv *= rgbFix;
            gv *= rgbFix;
            rv *= rgbFix;

            //0~255の範囲を超えることがあるので、修正
            bv = bv < 0 ? 0 : bv > 255 ? 255 : bv;
            gv = gv < 0 ? 0 : gv > 255 ? 255 : gv;
            rv = rv < 0 ? 0 : rv > 255 ? 255 : rv;
            av = av < 0 ? 0 : av > 255 ? 255 : av;
            pp = (y * stride) + (x * pByte);
            pixels[pp] = (byte)(bv + 0.5);
            pixels[pp + 1] = (byte)(gv + 0.5);
            pixels[pp + 2] = (byte)(rv + 0.5);
            pixels[pp + 3] = (byte)(av + 0.5);
        }
    };

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


AlphaもRGBと同じように処理したら
このときと同じ不具合が出たので

f:id:gogowaten:20210425154502p:plain
参照範囲に完全透明ピクセルがあった場合
同じ処理をしている
参照範囲の完全透明ピクセルはRGB値を加算しないで、Alpha値だけ別に加算集計しておいて268行目
参照範囲の集計が終わった後に、285行目~
1/(1-Alpha加算別集計)
これをRGB各値の掛け算して修正している




テストアプリ

f:id:gogowaten:20210425152755p:plain
テストアプリ
画像ファイルドロップかクリップボードからの貼り付けで画像表示
画像表示しないでボタン押すとエラーで止まる
倍率=2のとき縮小ボタン押すと1/2になる、倍率=10なら1/10
倍率は1~10
処理完了時に左下に処理時間と使ったメソッド名が表示される



作成動作環境




ダウンロード

github.com
ここの20210425_.zip

2021/04/26追記
上のはクリップボードからの貼り付けして32bitで変換するとエラーになっていたので
20210425_.1.0.1.zip




コード

MainWindow.xaml

<Window x:Class="_20210425_バイキュービックカラー版.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:_20210425_バイキュービックカラー版"
        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">
            <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="縮小(24bit)" Click="MyButton1_Click"/>
        <Button x:Name="MyButton2" Content="拡大(24bit)" Click="MyButton2_Click"/>
        <Button x:Name="MyButton3" Content="縮小(32bit)" Click="MyButton3_Click"/>
        <Button x:Name="MyButton4" Content="拡大(32bit)" Click="MyButton4_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="-0.5" Width="100" 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"/>
        <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.Media;
using System.Windows.Media.Imaging;

using System.Diagnostics;

namespace _20210425_バイキュービックカラー版
{
    public partial class MainWindow : Window
    {
        private BitmapSource MyBitmapOrigin;
        private BitmapSource MyBitmapOrigin32bit;
        public MainWindow()
        {
            InitializeComponent();
#if DEBUG
            this.Top = 0;
            this.Left = 0;
#endif
            this.Background = MakeTileBrush(MakeCheckeredPattern(16, Colors.WhiteSmoke, Colors.LightGray));
        }

        //処理時間計測
        private void MyExe(Func<BitmapSource, int, int, double, BitmapSource> func,
    BitmapSource source, int width, int height, double 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;
        }



        /// <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.Bgra32専用)
        /// セパラブルとParallelによる高速化
        /// </summary>
        /// <param name="source">PixelFormats.Bgra32のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicBgra32Kai(BitmapSource source, int width, int height, double a = -1.0)
        {
            //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 bv = 0, gv = 0, rv = 0, av = 0;
                      double alphaFix = 0;//完全透明ピクセルによる修正用
                      int pp;
                      for (int xx = -2; xx <= 1; xx++)
                      {
                          int xc = xKijun + xx;
                          //マイナス座標や画像サイズを超えていたら、収まるように修正
                          xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                          //距離計算、+0.5しているのは中心座標で計算するため
                          double dx = Math.Abs(rx - (xx + xKijun + 0.5));
                          //重み取得してrgb各値と掛け算して加算
                          double weight = GetWeightCubic(dx, a);
                          pp = (y * sourceStride) + (xc * pByte);
                          //完全透明ピクセル(a=0)だった場合はRGBは計算しないで
                          //重みだけ足し算して後で使う
                          if (sourcePixels[pp + 3] == 0)
                          {
                              alphaFix += weight;
                              continue;
                          }
                          bv += sourcePixels[pp] * weight;
                          gv += sourcePixels[pp + 1] * weight;
                          rv += sourcePixels[pp + 2] * weight;
                          av += sourcePixels[pp + 3] * weight;
                      }

                      //                    C#、WPF、バイリニア法での画像の拡大縮小変換、半透明画像(32bit画像)対応版 - 午後わてんのブログ
                      //https://gogowaten.hatenablog.com/entry/2021/04/17/151803#32bit%E3%81%A824bit%E3%81%AF%E9%81%95%E3%81%A3%E3%81%9F

                      //完全透明ピクセルによるRGB値の修正
                      //16ピクセルすべて完全透明だった場合は計算しない0のまま
                      if (alphaFix == 1) continue;
                      //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する
                      double rgbFix = 1 / (1 - alphaFix);
                      bv *= rgbFix;
                      gv *= rgbFix;
                      rv *= rgbFix;

                      pp = y * stride + x * pByte;
                      xResult[pp] = bv;
                      xResult[pp + 1] = gv;
                      xResult[pp + 2] = rv;
                      xResult[pp + 3] = av;
                  }
              });

            //縦処理
            _ = 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 bv = 0, gv = 0, rv = 0, av = 0;
                      double alphaFix = 0;
                      int pp;
                      for (int yy = -2; yy <= 1; yy++)//
                      {
                          double dy = Math.Abs(ry - (yy + yKijun + 0.5));//距離
                          double weight = GetWeightCubic(dy, a);//重み
                          int yc = yKijun + yy;
                          yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                          pp = (yc * stride) + (x * pByte);

                          //完全透明ピクセル(a=0)だった場合はRGBは計算しないで
                          //重みだけ足し算して後で使う
                          if (xResult[pp + 3] == 0)
                          {
                              alphaFix += weight;
                              continue;
                          }
                          bv += xResult[pp] * weight;
                          gv += xResult[pp + 1] * weight;
                          rv += xResult[pp + 2] * weight;
                          av += xResult[pp + 3] * weight;
                      }
                      //完全透明ピクセルによるRGB値の修正
                      //16ピクセルすべて完全透明だった場合は計算しない0のまま
                      if (alphaFix == 1) continue;
                      //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する
                      double rgbFix = 1 / (1 - alphaFix);
                      bv *= rgbFix;
                      gv *= rgbFix;
                      rv *= rgbFix;
                      //0~255の範囲を超えることがあるので、修正
                      bv = bv < 0 ? 0 : bv > 255 ? 255 : bv;
                      gv = gv < 0 ? 0 : gv > 255 ? 255 : gv;
                      rv = rv < 0 ? 0 : rv > 255 ? 255 : rv;
                      av = av < 0 ? 0 : av > 255 ? 255 : av;
                      int ap = (y * stride) + (x * pByte);
                      pixels[ap] = (byte)(bv + 0.5);
                      pixels[ap + 1] = (byte)(gv + 0.5);
                      pixels[ap + 2] = (byte)(rv + 0.5);
                      pixels[ap + 3] = (byte)(av + 0.5);
                  }
              });


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



        //未使用
        /// <summary>
        /// 画像の拡大縮小、バイキュービック法で補完、PixelFormats.Bgra32専用)
        /// 通常版
        /// </summary>
        /// <param name="source">PixelFormats.Bgra32のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicBgra32(BitmapSource source, int width, int height, double a = -1.0)
        {
            //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];

            int pp;
            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 bv = 0, gv = 0, rv = 0, av = 0;
                    double alphaFix = 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;
                            double weight = yw * xw;
                            pp = (yc * sourceStride) + (xc * pByte);
                            //完全透明ピクセル(a=0)だった場合はRGBは計算しないで
                            //重みだけ足し算して後で使う
                            if (sourcePixels[pp + 3] == 0)
                            {
                                alphaFix += weight;
                                continue;
                            }
                            bv += sourcePixels[pp] * weight;
                            gv += sourcePixels[pp + 1] * weight;
                            rv += sourcePixels[pp + 2] * weight;
                            av += sourcePixels[pp + 3] * weight;
                        }
                    }

                    //                    C#、WPF、バイリニア法での画像の拡大縮小変換、半透明画像(32bit画像)対応版 - 午後わてんのブログ
                    //https://gogowaten.hatenablog.com/entry/2021/04/17/151803#32bit%E3%81%A824bit%E3%81%AF%E9%81%95%E3%81%A3%E3%81%9F

                    //完全透明ピクセルによるRGB値の修正
                    //16ピクセルすべて完全透明だった場合は計算しない0のまま
                    if (alphaFix == 1) continue;
                    //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する
                    double rgbFix = 1 / (1 - alphaFix);
                    bv *= rgbFix;
                    gv *= rgbFix;
                    rv *= rgbFix;

                    //0~255の範囲を超えることがあるので、修正
                    bv = bv < 0 ? 0 : bv > 255 ? 255 : bv;
                    gv = gv < 0 ? 0 : gv > 255 ? 255 : gv;
                    rv = rv < 0 ? 0 : rv > 255 ? 255 : rv;
                    av = av < 0 ? 0 : av > 255 ? 255 : av;
                    pp = (y * stride) + (x * pByte);
                    pixels[pp] = (byte)(bv + 0.5);
                    pixels[pp + 1] = (byte)(gv + 0.5);
                    pixels[pp + 2] = (byte)(rv + 0.5);
                    pixels[pp + 3] = (byte)(av + 0.5);
                }
            };

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




        /// <summary>
        /// 画像の拡大縮小、バイキュービック法で補完、PixelFormats.Bgr24専用)
        /// セパラブルとParallelで高速化
        /// </summary>
        /// <param name="source">PixelFormats.Bgr24のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicBgr24Kai(BitmapSource source, int width, int height, double a = -1.0)
        {
            //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 bv = 0, gv = 0, rv = 0;
                    int pp;
                    for (int xx = -2; xx <= 1; xx++)
                    {
                        int xc = xKijun + xx;
                        //マイナス座標や画像サイズを超えていたら、収まるように修正
                        xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc;
                        //距離計算、+0.5しているのは中心座標で計算するため
                        double dx = Math.Abs(rx - (xx + xKijun + 0.5));
                        //重み取得してrgb各値と掛け算して加算
                        double weight = GetWeightCubic(dx, a);
                        pp = (y * sourceStride) + (xc * pByte);
                        bv += sourcePixels[pp] * weight;
                        gv += sourcePixels[pp + 1] * weight;
                        rv += sourcePixels[pp + 2] * weight;
                    }
                    pp = y * stride + x * pByte;
                    xResult[pp] = bv;
                    xResult[pp + 1] = gv;
                    xResult[pp + 2] = rv;
                }
            });

            //縦処理
            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 bv = 0, gv = 0, rv = 0;
                    int pp;
                    for (int yy = -2; yy <= 1; yy++)//
                    {
                        double dy = Math.Abs(ry - (yy + yKijun + 0.5));//距離
                        double weight = GetWeightCubic(dy, a);//重み
                        int yc = yKijun + yy;
                        yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc;
                        pp = (yc * stride) + (x * pByte);
                        bv += xResult[pp] * weight;
                        gv += xResult[pp + 1] * weight;
                        rv += xResult[pp + 2] * weight;
                    }
                    //0~255の範囲を超えることがあるので、修正
                    bv = bv < 0 ? 0 : bv > 255 ? 255 : bv;
                    gv = gv < 0 ? 0 : gv > 255 ? 255 : gv;
                    rv = rv < 0 ? 0 : rv > 255 ? 255 : rv;
                    int ap = (y * stride) + (x * pByte);
                    pixels[ap] = (byte)(bv + 0.5);
                    pixels[ap + 1] = (byte)(gv + 0.5);
                    pixels[ap + 2] = (byte)(rv + 0.5);
                }
            });


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



        //未使用
        /// <summary>
        /// 画像の拡大縮小、バイキュービック法で補完、PixelFormats.Bgr24専用)
        /// 通常版
        /// </summary>
        /// <param name="source">PixelFormats.Bgr24のBitmap</param>
        /// <param name="width">変換後の横ピクセル数を指定</param>
        /// <param name="height">変換後の縦ピクセル数を指定</param>
        /// <param name="a">-0.5~-1.0くらいを指定する、小さくするとシャープ、大きくするとぼかし</param>
        /// <returns></returns>
        private BitmapSource BicubicBgr24(BitmapSource source, int width, int height, double a = -1.0)
        {
            //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 bv = 0, gv = 0, rv = 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;
                            double weight = yw * xw;
                            int pp = (yc * sourceStride) + (xc * pByte);
                            bv += sourcePixels[pp] * weight;
                            gv += sourcePixels[pp + 1] * weight;
                            rv += sourcePixels[pp + 2] * weight;
                        }
                    }
                    //0~255の範囲を超えることがあるので、修正
                    bv = bv < 0 ? 0 : bv > 255 ? 255 : bv;
                    gv = gv < 0 ? 0 : gv > 255 ? 255 : gv;
                    rv = rv < 0 ? 0 : rv > 255 ? 255 : rv;
                    int ap = (y * stride) + (x * pByte);
                    pixels[ap] = (byte)(bv + 0.5);
                    pixels[ap + 1] = (byte)(gv + 0.5);
                    pixels[ap + 2] = (byte)(rv + 0.5);
                }
            };

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


        /// <summary>
        /// 画像ファイルパスからPixelFormats.Bgra32のBitmapSource作成
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="dpiX"></param>
        /// <param name="dpiY"></param>
        /// <returns></returns>
        private BitmapSource MakeBitmapSourceBgra32FromFile(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.Bgra32)
                    {
                        source = new FormatConvertedBitmap(source, PixelFormats.Bgra32, 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;
        }

        /// <summary>
        /// 画像ファイルパスからPixelFormats.Bgr24のBitmapSource作成
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="dpiX"></param>
        /// <param name="dpiY"></param>
        /// <returns></returns>
        private BitmapSource MakeBitmapSourceBgr24FromFile(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.Bgr24)
                    {
                        source = new FormatConvertedBitmap(source, PixelFormats.Bgr24, 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;
        }


        /// <summary>
        /// 市松模様の元になる画像作成、2色を2マスずつ合計4マス交互に並べた画像、
        /// □■
        /// ■□
        /// </summary>
        /// <param name="cellSize">1マスの1辺の長さ、作成される画像はこれの2倍の1辺になる</param>
        /// <param name="c1">色1</param>
        /// <param name="c2">色2</param>
        /// <returns>画像のピクセルフォーマットはBgra32</returns>
        private WriteableBitmap MakeCheckeredPattern(int cellSize, Color c1, Color c2)
        {
            int width = cellSize * 2;
            int height = cellSize * 2;
            var wb = new WriteableBitmap(width, height, 96, 96, PixelFormats.Bgra32, null);
            int stride = 4 * width;// wb.Format.BitsPerPixel / 8 * width;
            byte[] pixels = new byte[stride * height];
            //すべてを1色目で塗る
            for (int i = 0; i < pixels.Length; i += 4)
            {
                pixels[i] = c1.B;
                pixels[i + 1] = c1.G;
                pixels[i + 2] = c1.R;
                pixels[i + 3] = c1.A;
            }

            //2色目で市松模様にする
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    //左上と右下に塗る
                    if ((y < cellSize & x < cellSize) | (y >= cellSize & x >= cellSize))
                    {
                        int p = y * stride + x * 4;
                        pixels[p] = c2.B;
                        pixels[p + 1] = c2.G;
                        pixels[p + 2] = c2.R;
                        pixels[p + 3] = c2.A;
                    }
                }
            }
            wb.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
            return wb;
        }

        /// <summary>
        /// BitmapからImageBrush作成
        /// 引き伸ばし無しでタイル状に敷き詰め
        /// </summary>
        /// <param name="bitmap"></param>
        /// <returns></returns>
        private ImageBrush MakeTileBrush(BitmapSource bitmap)
        {
            var imgBrush = new ImageBrush(bitmap);
            imgBrush.Stretch = Stretch.None;//これは必要ないかも
            //タイルモード、タイル
            imgBrush.TileMode = TileMode.Tile;
            //タイルサイズは元画像のサイズ
            imgBrush.Viewport = new Rect(0, 0, bitmap.PixelWidth, bitmap.PixelHeight);
            //タイルサイズ指定方法は絶対値、これで引き伸ばされない
            imgBrush.ViewportUnits = BrushMappingMode.Absolute;
            return imgBrush;
        }



        //ファイルドロップ時
        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 = MakeBitmapSourceBgr24FromFile(paths[0]);
            MyBitmapOrigin32bit = MakeBitmapSourceBgra32FromFile(paths[0]);
            MyImage.Source = MyBitmapOrigin32bit;
        }

        //ボタンクリック
        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(BicubicBgr24Kai, 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(BicubicBgr24Kai, 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(BicubicBgra32Kai, MyBitmapOrigin32bit, 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(BicubicBgra32Kai, MyBitmapOrigin32bit, 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 img = GetImageFromClipboardWithPNG();
            if (img != null)
            {
                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, System.Windows.Input.MouseWheelEventArgs e)
        {
            System.Windows.Controls.Slider slider = sender as System.Windows.Controls.Slider;
            if (e.Delta > 0) slider.Value += slider.SmallChange;
            else slider.Value -= slider.SmallChange;
        }


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


        #endregion コピペ
    }
}

テストアプリでは昨日の記事からParallelによる並列化とセパラブルで高速化して使っている




テスト

24bit、普通のjpeg画像

f:id:gogowaten:20210424200647j:plain
普通の写真画像

f:id:gogowaten:20210425164756p:plain
アプリに表示したところ

f:id:gogowaten:20210425164948p:plainf:id:gogowaten:20210425164952p:plain
1/3と1/10に縮小したところ

拡大

f:id:gogowaten:20210425165307p:plainf:id:gogowaten:20210425165312p:plain
3倍と10倍に拡大

定数と画質の変化

f:id:gogowaten:20210425170220p:plain
定数と画質
小さくするとシャープになるというかガタガタになるねえ、ウィキペディアにあったように-0.5~-0.75がいい
10倍拡大だともっと違いがはっきりするんだけど、連続して処理してたら、エラーも出さずにアプリが落ちてしまったので5倍拡大で比較した

32bit(半透明)画像

f:id:gogowaten:20210425171149p:plain
半透明画像
完全透明ピクセルもある画像

f:id:gogowaten:20210425171321p:plain
アプリに表示
背景の市松模様が透けて見えている

縮小

f:id:gogowaten:20210425171441p:plain
1/3
期待通りに縮小できている、いいね

画像だけ表示してみると

f:id:gogowaten:20210425171649p:plain
1/3

半透明画像を24bitで処理すると

f:id:gogowaten:20210425182003p:plain
半透明画像を24bitで処理
透明度のAlphaが失われてこうなる
画像単体だと
f:id:gogowaten:20210425182108p:plain
失われた半透明


32bitで拡大処理

f:id:gogowaten:20210425182232p:plain
3倍拡大処理
画像単体だと
f:id:gogowaten:20210425182313p:plain
3倍拡大処理
期待通り、いいね!

f:id:gogowaten:20210425183727p:plain
3倍拡大処理


f:id:gogowaten:20210426110413p:plain
バイリニアとバイキュービック
f:id:gogowaten:20210426110606p:plain
どちらも同じに見える
1/3倍に縮小をバイリニアとバイキュービックでの比較なんだけど、同じに見える
拡大処理だと違うんだけどねえ




感想

メモリが足りない

f:id:gogowaten:20210425152833p:plain
メモリが足りなくてエラー?
1088x816を10倍拡大処理をテストしていたらエラーが出た、メモリが足りない感じ
実際にスマホで撮った写真画像を10倍拡大する事あるのかっていったらないんだけど、アプリ作ってるときのテストではするからねえ、やっぱり16GBじゃ足りない、32GBほしい

どれくらいのメモリ使うのか
10880 * 8160=88780800ピクセル、24bit(3byte)だったとしても
88780800 * 3 = 266342400Byte
266342400 / 1000 = 266342.4KB
266342.4 / 1000 = 266.3424MB

32bit処理なら266.3424 * 4 / 3 = 355.1232MB
でも、実際にはこれの倍くらい使っている感じかなあ

バイキュービックの拡大処理はバイリニアよりきれいだけど、縮小処理では違いがわからなかった
処理時間はバイリニアのほうが速いから、バイキュービックは拡大専用って感じ




関連記事

次回は明日
gogowaten.hatenablog.com

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

一昨々日
gogowaten.hatenablog.com

10日前
gogowaten.hatenablog.com