午後わてんのブログ

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

C#、WPF、バイリニア法での画像の拡大縮小変換、半透明画像(32bit画像)対応版

昨日の24bit対応版を改変

コード

縮小変換専用

//縮小専用
/// <summary>
/// 画像の縮小、バイリニア法で補完、PixelFormats.Bgra32専用)
/// </summary>
/// <param name="source">PixelFormats.Bgra32のBitmap</param>
/// <param name="yoko">変換後の横ピクセル数を指定</param>
/// <param name="tate">変換後の縦ピクセル数を指定</param>
/// <returns></returns>
private BitmapSource BilinearBgra32縮小専用(BitmapSource source, int yoko, int tate)
{
    //元画像の画素値の配列作成
    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];
    int pByte = (source.Format.BitsPerPixel + 7) / 8;//1ピクセルあたりのバイト数、Byte / Pixel

    _ = Parallel.For(0, tate, y =>
    {
        for (int x = 0; x < yoko; x++)
        {
            //参照範囲の左上座標bp
            double bpX = ((x + 0.5) * yokoScale) - 0.5;
            double bpY = ((y + 0.5) * tateScale) - 0.5;

            //小数部分s
            double sx = bpX % 1;
            double sy = bpY % 1;

            //面積
            double d = sx * sy;
            double c = (1 - sx) * sy;
            double b = sx * (1 - sy);
            double a = 1 - (d + c + b);// (1 - sx) * (1 - sy)

            //左上ピクセルの座標は
            //参照範囲の左上座標の小数部分を切り捨て(整数部分)
            //左上ピクセルのIndex
            int ia = ((int)bpY * stride) + ((int)bpX * pByte);
            int ib = ia + pByte;
            int ic = ((int)bpY * stride + stride) + ((int)bpX * pByte);
            int id = ((int)bpY * stride + stride) + ((int)bpX * pByte) + pByte;

            //Alphaが0の区画のRGB値は無視したいので初期値1.0から面積を引き算して
            //有効面積率を計算
            byte aAlpha = pixels[ia + 3];
            byte bAlpha = pixels[ia + pByte + 3];
            byte cAlpha = pixels[ia + stride + 3];
            byte dAlpha = pixels[ia + stride + pByte + 3];
            //有効面積率
            double effectiveAreaRatio = 1.0;
            if (aAlpha == 0) effectiveAreaRatio -= a;
            if (bAlpha == 0) effectiveAreaRatio -= b;
            if (cAlpha == 0) effectiveAreaRatio -= c;
            if (dAlpha == 0) effectiveAreaRatio -= d;
            //1.0/有効面積率、これをRGB合計値に掛け算
            effectiveAreaRatio = 1.0 / effectiveAreaRatio;

            //各区の値*面積の合計に有効面積倍率を掛け算してから四捨五入して完成
            //Blue
            resultPixels[y * scaledStride + x * pByte] =
                  (byte)(((
                  pixels[ia] * a
                  + pixels[ia + pByte] * b
                  + pixels[ia + stride] * c
                  + pixels[ia + stride + pByte] * d
                  ) * effectiveAreaRatio
                  ) + 0.5);
            //Green
            resultPixels[y * scaledStride + x * pByte + 1] =
                  (byte)(((pixels[ia + 1] * a
                  + pixels[ia + pByte + 1] * b
                  + pixels[ia + stride + 1] * c
                  + pixels[ia + stride + pByte + 1] * d) * effectiveAreaRatio)
                  + 0.5);
            //Red
            resultPixels[y * scaledStride + x * pByte + 2] =
                  (byte)(((pixels[ia + 2] * a
                  + pixels[ia + pByte + 2] * b
                  + pixels[ia + stride + 2] * c
                  + pixels[ia + stride + pByte + 2] * d) * effectiveAreaRatio)
                  + 0.5);
            //Alpha
            resultPixels[y * scaledStride + x * pByte + 3] =
            (byte)((aAlpha * a) + (bAlpha * b) + (cAlpha * c) + (dAlpha * d) + 0.5);
        }
    });


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




拡大縮小両対応

/// <summary>
/// 画像の拡大縮小、バイリニア法で補完(PixelFormats.Bgra32専用)
/// </summary>
/// <param name="source">PixelFormats.Bgra32のBitmap</param>
/// <param name="yoko">横ピクセル数を指定</param>
/// <param name="tate">縦ピクセル数を指定</param>
/// <returns></returns>
private BitmapSource BilinearBgra32専用(BitmapSource source, int yoko, int tate)
{
    //元画像の画素値の配列作成
    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;
    //1ピクセルあたりのバイト数、Byte / Pixel
    int pByte = (source.Format.BitsPerPixel + 7) / 8;

    int scaledStride = yoko * pByte;
    byte[] resultPixels = new byte[tate * scaledStride];
    Parallel.For(0, tate, y =>
    {
        for (int x = 0; x < yoko; x++)
        {
            //参照範囲の左上座標bp
            double bpX = ((x + 0.5) * yokoScale) - 0.5;
            //画像範囲内チェック、参照範囲が画像から外れていたら修正(収める)
            if (bpX < 0) { bpX = 0; }
            if (bpX > sourceWidth - 1) { bpX = sourceWidth - 1; }

            double bpY = (y + 0.5) * tateScale - 0.5;
            if (bpY < 0) { bpY = 0; }
            if (bpY > sourceHeight - 1) { bpY = sourceHeight - 1; }

            //小数部分s
            double sx = bpX % 1;
            double sy = bpY % 1;

            //面積
            double d = sx * sy;
            double c = (1 - sx) * sy;
            double b = sx * (1 - sy);
            double a = 1 - (d + c + b);// (1 - sx) * (1 - sy)

            //左上ピクセルの座標は
            //参照範囲の左上座標の小数部分を切り捨て(整数部分)
            //左上ピクセルのIndex
            int i = ((int)bpY * stride) + ((int)bpX * pByte);


            //値*面積
            double aBlue = pixels[i] * a;
            double aGreen = pixels[i + 1] * a;
            double aRed = pixels[i + 2] * a;
            double aAlpha = pixels[i + 3] * a;

            //Alphaが0の区画のRGB値は無視したいので初期値1.0から面積を引き算して
            //有効面積率を計算
            double effectiveAreaRatio = 1.0;
            if (pixels[i + 3] == 0) effectiveAreaRatio -= a;

            double bB = 0;
            double bG = 0;
            double bR = 0;
            double bA = 0;

            double cB = 0;
            double cG = 0;
            double cR = 0;
            double cA = 0;

            double dB = 0;
            double dG = 0;
            double dR = 0;
            double dA = 0;

            int pp;
            //B区以降は面積が0より大きいときだけ計算
            if (b != 0)
            {
                //Aの右ピクセル*Bの面積
                pp = i + pByte;
                bB = pixels[pp] * b;
                bG = pixels[pp + 1] * b;
                bR = pixels[pp + 2] * b;
                bA = pixels[pp + 3] * b;
                if (pixels[pp + 3] == 0) effectiveAreaRatio -= b;
            }
            if (c != 0)
            {
                //下側ピクセル
                pp = i + stride;
                cB = pixels[pp] * c;
                cG = pixels[pp + 1] * c;
                cR = pixels[pp + 2] * c;
                cA = pixels[pp + 3] * c;
                if (pixels[pp + 3] == 0) effectiveAreaRatio -= c;
            }
            if (d != 0)
            {
                //Aの右下ピクセル、仮にAが画像右下ピクセルだったとしても
                //そのときは面積が0のはずだからここは計算されない
                pp = i + stride + pByte;
                dB = pixels[pp] * d;
                dG = pixels[pp + 1] * d;
                dR = pixels[pp + 2] * d;
                dA = pixels[pp + 3] * d;
                if (pixels[pp + 3] == 0) effectiveAreaRatio -= d;
            }

            //Alpha0の面積によって倍率変更
            //有効面積率はRGBそれぞれに掛け算
            effectiveAreaRatio = 1 / effectiveAreaRatio;

            //4区を合計して四捨五入で完成
            resultPixels[(y * scaledStride) + (x * pByte)] = (byte)(((aBlue + bB + cB + dB) * effectiveAreaRatio) + 0.5);
            resultPixels[(y * scaledStride) + (x * pByte) + 1] = (byte)(((aGreen + bG + cG + dG) * effectiveAreaRatio) + 0.5);
            resultPixels[(y * scaledStride) + (x * pByte) + 2] = (byte)(((aRed + bR + cR + dR) * effectiveAreaRatio) + 0.5);
            resultPixels[(y * scaledStride) + (x * pByte) + 3] = (byte)(aAlpha + bA + cA + dA + 0.5);
        }
    });

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




テストアプリ

f:id:gogowaten:20210417105850p:plain
テストアプリ
半透明画像の確認がしやすいように背景に市松模様を敷いてみた

github.com




作成動作環境




MainWindow.xaml

<Window x:Class="_20210417_Bilinearで画像拡大縮小32bitカラー.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:_20210417_Bilinearで画像拡大縮小32bitカラー"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="614"
        AllowDrop="True" Drop="Window_Drop">
  <Grid>
    <DockPanel UseLayoutRounding="True">
      <StackPanel DockPanel.Dock="Right">
        <Button x:Name="MyButton1" Content="1/2倍" Click="MyButton1_Click"/>
        <Button x:Name="MyButton2" Content="1/3倍" Click="MyButton2_Click"/>
        <Button x:Name="MyButton3" Content="2倍" Click="MyButton3_Click"/>
        <Button x:Name="MyButton4" Content="3倍" Click="MyButton4_Click"/>
        <Button x:Name="MyButton5" Content="1/5倍" Click="MyButton5_Click"/>
        <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.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace _20210417_Bilinearで画像拡大縮小32bitカラー
{
    public partial class MainWindow : Window
    {
        private BitmapSource MyBitmapOrigin;
        public MainWindow()
        {
            InitializeComponent();
#if DEBUG
            this.Top = 0;
            this.Left = 0;
#endif
            this.Background = MakeTileBrush(MakeCheckeredPattern(10, Colors.WhiteSmoke, Colors.LightGray));
        }


        //縮小拡大対応完成版
        /// <summary>
        /// 画像の拡大縮小、バイリニア法で補完(PixelFormats.Bgra32専用)
        /// </summary>
        /// <param name="source">PixelFormats.Bgra32のBitmap</param>
        /// <param name="yoko">横ピクセル数を指定</param>
        /// <param name="tate">縦ピクセル数を指定</param>
        /// <returns></returns>
        private BitmapSource BilinearBgra32専用(BitmapSource source, int yoko, int tate)
        {
            //元画像の画素値の配列作成
            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;
            //1ピクセルあたりのバイト数、Byte / Pixel
            int pByte = (source.Format.BitsPerPixel + 7) / 8;

            int scaledStride = yoko * pByte;
            byte[] resultPixels = new byte[tate * scaledStride];
            Parallel.For(0, tate, y =>
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照範囲の左上座標bp
                    double bpX = ((x + 0.5) * yokoScale) - 0.5;
                    //画像範囲内チェック、参照範囲が画像から外れていたら修正(収める)
                    if (bpX < 0) { bpX = 0; }
                    if (bpX > sourceWidth - 1) { bpX = sourceWidth - 1; }

                    double bpY = (y + 0.5) * tateScale - 0.5;
                    if (bpY < 0) { bpY = 0; }
                    if (bpY > sourceHeight - 1) { bpY = sourceHeight - 1; }

                    //小数部分s
                    double sx = bpX % 1;
                    double sy = bpY % 1;

                    //面積
                    double d = sx * sy;
                    double c = (1 - sx) * sy;
                    double b = sx * (1 - sy);
                    double a = 1 - (d + c + b);// (1 - sx) * (1 - sy)

                    //左上ピクセルの座標は
                    //参照範囲の左上座標の小数部分を切り捨て(整数部分)
                    //左上ピクセルのIndex
                    int i = ((int)bpY * stride) + ((int)bpX * pByte);


                    //値*面積
                    double aBlue = pixels[i] * a;
                    double aGreen = pixels[i + 1] * a;
                    double aRed = pixels[i + 2] * a;
                    double aAlpha = pixels[i + 3] * a;

                    //Alphaが0の区画のRGB値は無視したいので初期値1.0から面積を引き算して
                    //有効面積率を計算
                    double effectiveAreaRatio = 1.0;
                    if (pixels[i + 3] == 0) effectiveAreaRatio -= a;

                    double bB = 0;
                    double bG = 0;
                    double bR = 0;
                    double bA = 0;

                    double cB = 0;
                    double cG = 0;
                    double cR = 0;
                    double cA = 0;

                    double dB = 0;
                    double dG = 0;
                    double dR = 0;
                    double dA = 0;

                    int pp;
                    //B区以降は面積が0より大きいときだけ計算
                    if (b != 0)
                    {
                        //Aの右ピクセル*Bの面積
                        pp = i + pByte;
                        bB = pixels[pp] * b;
                        bG = pixels[pp + 1] * b;
                        bR = pixels[pp + 2] * b;
                        bA = pixels[pp + 3] * b;
                        if (pixels[pp + 3] == 0) effectiveAreaRatio -= b;
                    }
                    if (c != 0)
                    {
                        //下側ピクセル
                        pp = i + stride;
                        cB = pixels[pp] * c;
                        cG = pixels[pp + 1] * c;
                        cR = pixels[pp + 2] * c;
                        cA = pixels[pp + 3] * c;
                        if (pixels[pp + 3] == 0) effectiveAreaRatio -= c;
                    }
                    if (d != 0)
                    {
                        //Aの右下ピクセル、仮にAが画像右下ピクセルだったとしても
                        //そのときは面積が0のはずだからここは計算されない
                        pp = i + stride + pByte;
                        dB = pixels[pp] * d;
                        dG = pixels[pp + 1] * d;
                        dR = pixels[pp + 2] * d;
                        dA = pixels[pp + 3] * d;
                        if (pixels[pp + 3] == 0) effectiveAreaRatio -= d;
                    }

                    //Alpha0の面積によって倍率変更
                    //有効面積率はRGBそれぞれに掛け算
                    effectiveAreaRatio = 1 / effectiveAreaRatio;

                    //4区を合計して四捨五入で完成
                    resultPixels[(y * scaledStride) + (x * pByte)] = (byte)(((aBlue + bB + cB + dB) * effectiveAreaRatio) + 0.5);
                    resultPixels[(y * scaledStride) + (x * pByte) + 1] = (byte)(((aGreen + bG + cG + dG) * effectiveAreaRatio) + 0.5);
                    resultPixels[(y * scaledStride) + (x * pByte) + 2] = (byte)(((aRed + bR + cR + dR) * effectiveAreaRatio) + 0.5);
                    resultPixels[(y * scaledStride) + (x * pByte) + 3] = (byte)(aAlpha + bA + cA + dA + 0.5);
                }
            });

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



        //E:\オレ\エクセル\画像処理.xlsm_バイリニア法_$A$599
        //縮小専用
        /// <summary>
        /// 画像の縮小、バイリニア法で補完、PixelFormats.Bgra32専用)
        /// </summary>
        /// <param name="source">PixelFormats.Bgra32のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <returns></returns>
        private BitmapSource BilinearBgra32縮小専用(BitmapSource source, int yoko, int tate)
        {
            //元画像の画素値の配列作成
            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];
            int pByte = (source.Format.BitsPerPixel + 7) / 8;//1ピクセルあたりのバイト数、Byte / Pixel

            _ = Parallel.For(0, tate, y =>
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照範囲の左上座標bp
                    double bpX = ((x + 0.5) * yokoScale) - 0.5;
                    double bpY = ((y + 0.5) * tateScale) - 0.5;

                    //小数部分s
                    double sx = bpX % 1;
                    double sy = bpY % 1;

                    //面積
                    double d = sx * sy;
                    double c = (1 - sx) * sy;
                    double b = sx * (1 - sy);
                    double a = 1 - (d + c + b);// (1 - sx) * (1 - sy)

                    //左上ピクセルの座標は
                    //参照範囲の左上座標の小数部分を切り捨て(整数部分)
                    //左上ピクセルのIndex
                    int ia = ((int)bpY * stride) + ((int)bpX * pByte);
                    int ib = ia + pByte;
                    int ic = ((int)bpY * stride + stride) + ((int)bpX * pByte);
                    int id = ((int)bpY * stride + stride) + ((int)bpX * pByte) + pByte;

                    //Alphaが0の区画のRGB値は無視したいので初期値1.0から面積を引き算して
                    //有効面積率を計算
                    byte aAlpha = pixels[ia + 3];
                    byte bAlpha = pixels[ia + pByte + 3];
                    byte cAlpha = pixels[ia + stride + 3];
                    byte dAlpha = pixels[ia + stride + pByte + 3];
                    //有効面積率
                    double effectiveAreaRatio = 1.0;
                    if (aAlpha == 0) effectiveAreaRatio -= a;
                    if (bAlpha == 0) effectiveAreaRatio -= b;
                    if (cAlpha == 0) effectiveAreaRatio -= c;
                    if (dAlpha == 0) effectiveAreaRatio -= d;
                    //1.0/有効面積率、これをRGB合計値に掛け算
                    effectiveAreaRatio = 1.0 / effectiveAreaRatio;

                    //各区の値*面積の合計に有効面積倍率を掛け算してから四捨五入して完成
                    //Blue
                    resultPixels[y * scaledStride + x * pByte] =
                          (byte)(((
                          pixels[ia] * a
                          + pixels[ia + pByte] * b
                          + pixels[ia + stride] * c
                          + pixels[ia + stride + pByte] * d
                          ) * effectiveAreaRatio
                          ) + 0.5);
                    //Green
                    resultPixels[y * scaledStride + x * pByte + 1] =
                          (byte)(((pixels[ia + 1] * a
                          + pixels[ia + pByte + 1] * b
                          + pixels[ia + stride + 1] * c
                          + pixels[ia + stride + pByte + 1] * d) * effectiveAreaRatio)
                          + 0.5);
                    //Red
                    resultPixels[y * scaledStride + x * pByte + 2] =
                          (byte)(((pixels[ia + 2] * a
                          + pixels[ia + pByte + 2] * b
                          + pixels[ia + stride + 2] * c
                          + pixels[ia + stride + pByte + 2] * d) * effectiveAreaRatio)
                          + 0.5);
                    //Alpha
                    resultPixels[y * scaledStride + x * pByte + 3] =
                    (byte)((aAlpha * a) + (bAlpha * b) + (cAlpha * c) + (dAlpha * d) + 0.5);
                }
            });


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


        /// <summary>
        /// 画像ファイルパスからPixelFormats.Bgra32のBitmapSource作成
        /// </summary>
        /// <param name="filePath"></param>
        /// <param name="dpiX"></param>
        /// <param name="dpiY"></param>
        /// <returns></returns>
        private BitmapSource MakeBitmapSourceGray8FromFile(string filePath, double dpiX = 96, double dpiY = 96)
        {
            BitmapSource source = null;
            try
            {
                using (var stream = System.IO.File.OpenRead(filePath))
                {
                    source = BitmapFrame.Create(stream);
                    if (source.Format != PixelFormats.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;
        }


        //        クリップボードに複数の形式のデータをコピーする - .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 = MakeBitmapSourceGray8FromFile(paths[0]);
            MyImage.Source = MyBitmapOrigin;
        }

        //ボタンクリック
        private void MyButton1_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / 2.0);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / 2.0);
            MyImage.Source = BilinearBgra32縮小専用(MyBitmapOrigin, yoko, tate);
        }


        private void MyButton2_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / 3.0);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / 3.0);
            MyImage.Source = BilinearBgra32専用(MyBitmapOrigin, yoko, tate);
        }

        private void MyButton3_Click(object sender, RoutedEventArgs e)
        {
            MyImage.Source = BilinearBgra32専用(MyBitmapOrigin,
                MyBitmapOrigin.PixelWidth * 2,
                MyBitmapOrigin.PixelHeight * 2);
        }

        private void MyButton4_Click(object sender, RoutedEventArgs e)
        {
            MyImage.Source = BilinearBgra32専用(MyBitmapOrigin,
                MyBitmapOrigin.PixelWidth * 3,
                MyBitmapOrigin.PixelHeight * 3);
        }

        //画像をクリップボードにコピー
        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)
            {
                MyBitmapOrigin = bitmap;
                MyImage.Source = bitmap;
            }
        }

        private void MyButton5_Click(object sender, RoutedEventArgs e)
        {
            int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / 5.0);
            int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / 5.0);
            MyImage.Source = BilinearBgra32専用(MyBitmapOrigin, yoko, tate);
        }
    }
}




テストアプリで確認

確認用画像

f:id:gogowaten:20210416144112p:plain
確認用画像
透明(A=0)から不透明(A=255)まである透明度のグラデーション画像

プロパティをみると

f:id:gogowaten:20210417110619p:plain
プロパティ
ビットの深さ32なので32bit画像

テストアプリに表示してみると

f:id:gogowaten:20210417110539p:plain
半透明画像をアプリに表示
左上が完全透明で、右下が不透明な画像を2枚重ねた画像のがわかる

縮小
1/2倍

f:id:gogowaten:20210417111527p:plain
1/2倍
f:id:gogowaten:20210417111541p:plain
1/2倍の結果
いいね!

1/3倍

f:id:gogowaten:20210417111727p:plain
1/3倍にしたところ
f:id:gogowaten:20210417111746p:plain
1/3倍の結果


拡大
2倍

f:id:gogowaten:20210417112345p:plain
2倍に拡大したところ

f:id:gogowaten:20210417112402p:plain
2倍に拡大した結果

3倍

f:id:gogowaten:20210417112458p:plain
3倍にしたところ

f:id:gogowaten:20210417112516p:plain
3倍にした結果
拡大もできた!

f:id:gogowaten:20210417143139p:plain
写真画像
普通の写真画像(24bit)も読み込み時にピクセルフォーマットを32bitに変換しておけば拡大縮小できる


こうして32bit画像の拡大縮小できるようになったわけだけど、ここまで来るまでには予想外の問題があった

32bitと24bitは違った

8bitグレースケールは輝度値が8bit(0~255)の表現
24bitはRGBがそれぞれ8bitで合計24bit
32bitはRGBに加えて透明度A(Alpha値)を加えたものでARGB各8bitで合計32bit、半透明が表現できる
昨日の8bitから24bitへの対応は簡単にできたから、今回の32bit対応も同じ感じだと思っていて普通に書いたら

f:id:gogowaten:20210416150631p:plain
なんか違う
これは1/3倍にしたところ、右下をよく見ると不自然な黒い線ができている
画像単独で表示してみる
f:id:gogowaten:20210417113722p:plain
右下に黒い線
┏の形をした黒い線がある
拡大してみると

f:id:gogowaten:20210417113836p:plain
拡大
右下だけじゃなくて左上にも┛の形をした薄い灰色の線ができているのがわかった

色を確認してみる
f:id:gogowaten:20210416151858p:plain
色の確認
黒線の色はARGB=127,84,118,8
隣の色はARGB=252,173,229,16
すべての値が隣の半分の数値になっている

変換前の元の画像の同じ場所の色を確認

f:id:gogowaten:20210416152327p:plain
元画像の色確認
透明部分はARGB=0,0,0,0
緑色はARGB=254,164,231,16
つまり不自然な黒線は、縮小時の参照範囲が緑と透明にまたがった形になって、その中間の色になったってこと、なので黒線は透明ピクセルとの境目に発生している
中間の値にするっているのは、そういう処理を書いているから間違っていないんだけどねえ
透明ピクセルは透明なんだけど、透明度Aを除いたRGBの値は0,0,0だから、これは黒、真っ黒、これが縮小処理でAが0以上になると出てきてしまって不自然な黒に見えるってことみたい

透明度Alphaの値をすべて255にして不透明にすると

f:id:gogowaten:20210417111004p:plain
Alphaすべてを255にした場合
こうなっている、左上と右下の透明部分は真っ黒(RGB=0,0,0)が指定されている

どうなればいいのかエクセルの図形に色塗って考えてみる

f:id:gogowaten:20210417141901p:plain
エクセルの図形に色塗り
f:id:gogowaten:20210417142458p:plain
図形の色の設定

黄色ARGB=255,255,255,0と透明の中間の色は?
試しに中間だからすべての値を中間にすると、かなり不自然な色になるのがわかったので、この方法は間違っている
じゃあどんな色なら自然なのか
Alphaの値は今まで通り距離に応じた値に変化させていいはずとして、1.5倍時の中間なら128
それ以外のRGBは、完全透明のピクセルの値は無視して、それ以外のピクセルの色を引き継ぐ
右の図がそれで、ARGB=128,255,255,0
これが自然だと思う、これがいい

Alphaは今まで通りでいいとして、RGBだけ別の計算になる、その計算はどうするのか
一昨日の記事から

f:id:gogowaten:20210415104818p:plainf:id:gogowaten:20210415112254p:plain
参照範囲の4区(4ピクセル)
参照範囲の4区のDが完全透明ピクセルだったときを考えると
f:id:gogowaten:20210417133042p:plain
また有効面積率とかおかしな独自用語を作り出してる
D区のRGBは無視して、その分を残りの3区の色で補うとか延ばすとか水増しする感じ
D区の面積率が0.4だった場合は、1.0からそれを引き算して
1.0 - 0.4 = 0.6
0.6を有効面積率として
この0.6を1.0に延ばすには
1.0を0.6で割り算
1.0 / 0.6 = 1.6666667
1.67倍すればいい
この倍率を、3区のRGBの各値*面積率の合計に掛け算して完成

図の場合のgreenを見ると、ABCDの値*面積率は
0, 127.5, 22.5, 0、合計で153、これが有効面積率の60%ぶんの値で、ここから100%にしたいから、1/0.6を掛け算して
153 * 1 / 0.6 = 255
これで完成

今思ったけど、これだと完全透明ピクセルが黒(0,0,0)前提で計算しているから、もし黒以外の完全透明だった場合は良くない結果になるなあ、でも完全透明ピクセルは多くの場合黒のはず


感想

32bit対応は思ってたより難しかった
これでバイリニア法での拡大縮小は完了
バイリニア法ってのは距離に直線的に比例して色を決めるらしいけど、実際にどう書いたらいいのかわからなくて試しているうちに面積にたどり着いた、面積も辺の長さが距離だから全く関係ないわけじゃないけど、やっぱり違うのかなあってのがある

それでも結果としては期待通り、想像通りの変換ができたのでかなり満足
3年前のもやもやを解消できたッ!
バイリニア法編完




関連記事

次回は6日後
gogowaten.hatenablog.com

前回は昨日
gogowaten.hatenablog.com

バイリニア法の再挑戦は一昨日から
gogowaten.hatenablog.com
画像処理の基本はグレースケールなんやねえ