午後わてんのブログ

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

C#、WPF、バイリニア法での画像の拡大縮小変換に再挑戦した結果、グレースケール専用

画像の縮小変換

Bilinearバイリニアってのは線形とか直線で、距離が近いほど影響が大きくなる感じ
3x3の画像を2x2に縮小するときにどんな感じになればいいのかエクセルを使って考えてみた

f:id:gogowaten:20210415103009p:plain
3x3を2x2へ縮小変換
変換後の画像のピクセルの値がどんな値になればいいのかは、そのピクセルの座標が元画像ではどの座標に相当するのかを計算することになる

f:id:gogowaten:20210415104818p:plain
どこを参照するのか
座標の変換は、もとのサイズ/変換後サイズ、これを倍率として掛け算する
3/2=1.5
(0,1)に1.5をかけると(0 * 1.5=0,1 * 1.5=1.5)で、(0,1.5)になる


ピクセルにも幅がある

f:id:gogowaten:20210415105006p:plain
ピクセルの中心座標を計算に使う
例えば(0,0)の場合だと、どんな倍率でも(0,0)にしかならないので、これは不自然
ピクセルにも幅があると考えて、その中心座標を使って計算する
中心座標はピクセル座標に0.5足すだけ
(0,0)の中心座標は(0.5,0.5)、(0,1)は(0.5,1.5)になる
この方法は以前の記事
最近傍補間法で画像の拡大縮小試してみた、2回め - 午後わてんのブログ
https://gogowaten.hatenablog.com/entry/2019/11/08/182906
このときに試してうまくいったので今回もそうしてみた


参照点

f:id:gogowaten:20210415110248p:plain
参照点
(0,1)の中心座標は(0.5,1.5)これに倍率の1.5をかけると
(0.5 * 1.5 = 0.75,1.5 * 1.5 = 2.25)なので
参照点は(0.75,2.25)


参照範囲は1x1

f:id:gogowaten:20210415110101p:plain
参照範囲
参照点を中心にした1x1の範囲を参照範囲にする
あとはこの範囲に重なるピクセルの値を、面積に応じて取り出せばそれが求める値になる
面積が大きい(参照点から近い)ほど直線的(リニア)に影響が大きくなるので、これがバイリニアってことなんだと思う
参照範囲の左上の座標をbpとして、これを参照範囲の基準点にする
参照点r(0.75,2.25)のbpは(0.25,1.75)


面積

f:id:gogowaten:20210415114228p:plain
4区の面積は
多くの場合参照範囲は4つのピクセルにまたがる形になる、それぞれの面積を求めるにはbpの小数部分を使う
bp(0.25,1.75)の小数部分はxが0.25、yが0.75、これをそれぞれsx、syとすると、ABCDの面積は
A (1-sx) * (1-sy)
B sx * (1-sy)
C (1-sx) * sy
D sx * sy
これで計算できる

結果は

面積 計算
A 0.1875 (1-0.25)*(1-0.75)=0.1875
B 0.0625 0.25*(1-0.75)=0.0625
C 0.5625 (1-0.25)*0.75=0.5625
D 0.1875 0.25*0.75=0.1875




ピクセル座標から値を取得

f:id:gogowaten:20210415114722p:plain
A区に重なるピクセル座標
bpの小数部分を切り捨てると、それがピクセル座標になる

f:id:gogowaten:20210415115427p:plain
BCDピクセル座標
Aのピクセル座標がわかればBCDはその隣なので1を足せばいい

f:id:gogowaten:20210415114911p:plain
各区の値を取得
ピクセル座標から値を取得

結果

A 200
B 180
C 150
D 130



答え

f:id:gogowaten:20210415115635p:plain
答え
各区の値と面積を掛け算して、合計したのが求める値になる
画素値は整数なので最後に四捨五入して完成

面積 値*面積
A 200 0.1875 37.5
B 180 0.0625 11.25
C 150 0.5625 84.375
D 130 0.1875 24.375

A+B+C+D=37.5+11.25+84.375+24.375=157.5
157.5を四捨五入して158

f:id:gogowaten:20210415120715p:plain
(0,1)の画素値は158



エクセルで確認

f:id:gogowaten:20210415120222p:plain
確認
(0,1)の画素値は158

f:id:gogowaten:20210415120854p:plain
参照元のトレース

f:id:gogowaten:20210415121009p:plain
(0,0)は233


中心座標じゃなくてピクセル座標で計算した場合

f:id:gogowaten:20210415122327p:plain
ピクセル中心座標で計算とピクセル座標で計算
(0,0)をそのまま計算するか、(0,0)の中心座標(0.5,0.5)で計算するかの違い
そのまま計算すると変換後の(0,0)は必ず元画像の(0,0)になる、これは倍率を変えても同じ、0に何かけても0にしかならない
試しに3x3を1x1に縮小変換つまり1ピクセルにしてみると
f:id:gogowaten:20210415124316p:plain
3x3を1x1に縮小変換
そのまま計算した場合はやっぱり250になった
中心座標で計算した場合は元画像の中央ピクセル(1,1)が参照範囲になって180になった、こっちのほうが自然


拡大変換に対応

縮小変換とは少し違う

さっきの変換を使って拡大してみるとエラーになる
3x3の画像を2倍の6x6に拡大変換するときに(5,5)のピクセルの値を求めてみると

f:id:gogowaten:20210415125400p:plain
エラー
エラーになる
これは参照範囲が元画像の範囲を超えてしまっているからで、これは右下の(5,5)だけに限らず、周縁部のピクセルは全部エラーになる、例えば左上(0,0)だとマイナスの値になって、画像にマイナス座標はないからエラーになる

f:id:gogowaten:20210415125558p:plain
参照範囲が元画像の範囲外
なので範囲外になった参照範囲を修正する必要がある、今回は一番近いピクセルに収めることにした

f:id:gogowaten:20210415130328p:plain
修正
bpの座標を見て0未満なら0に修正
bpのx座標を見て元画像の横ピクセル数-1より大きければ、bpのx座標を横ピクセル数-1に修正
bpのy座標を見て元画像の縦ピクセル数-1より大きければ、bpのy座標を縦ピクセル数-1に修正
ってことにした

f:id:gogowaten:20210415130844p:plain
拡大変換対応版
2.25は範囲外なので2に修正
BCDピクセルの値がエラーになっているけど、面積が0のときは面積*値を計算しないで、結果を0にする処理にしてある

f:id:gogowaten:20210415131439p:plain
(0,0)の場合
-0.25は0に修正されている


コード

縮小変換専用

/// <summary>
/// 画像の縮小、バイリニア法で補完、グレースケール専用(PixelFormats.Gray8専用)
/// </summary>
/// <param name="source">PixelFormats.Gray8のBitmap</param>
/// <param name="yoko">変換後の横ピクセル数を指定</param>
/// <param name="tate">変換後の縦ピクセル数を指定</param>
/// <returns></returns>
private BitmapSource BilinearGray8縮小専用(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];

    for (int y = 0; y < tate; y++)
    {
        for (int x = 0; x < yoko; x++)
        {
            //参照点r
            double rx = (x + 0.5) * yokoScale;
            double ry = (y + 0.5) * tateScale;

            //参照範囲の左上座標bp
            double bpX = rx - 0.5;
            double bpY = ry - 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 i = ((int)bpY * stride) + (int)bpX;

            //値*面積
            double aa = pixels[i] * a;
            double bb = pixels[i + 1] * b;
            double cc = pixels[i + stride] * c;
            double dd = pixels[i + stride + 1] * d;

            //4区を合計して四捨五入で完成
            double add = aa + bb + cc + dd;
            resultPixels[y * scaledStride + x] = (byte)(add + 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>
/// <returns></returns>
private BitmapSource BilinearGray8専用(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];

    for (int y = 0; y < tate; y++)
    {
        for (int x = 0; x < yoko; x++)
        {
            //参照点r
            double rx = (x + 0.5) * yokoScale;
            double ry = (y + 0.5) * tateScale;

            //参照範囲の左上座標bp
            double bpX = rx - 0.5;
            //画像範囲内チェック、参照範囲が画像から外れていたら修正(収める)
            if (bpX < 0) { bpX = 0; }
            if (bpX > sourceWidth - 1) { bpX = sourceWidth - 1; }

            double bpY = ry - 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 ia = ((int)bpY * stride) + (int)bpX;

            //値*面積
            double aa = pixels[ia] * a;
            double bb = 0, cc = 0, dd = 0;
            //B以降は面積が0より大きいときだけ計算
            if (b != 0)
            {
                //Aの右ピクセル*Bの面積
                bb = pixels[ia + 1] * b;
            }
            if (c != 0)
            {
                cc = pixels[ia + stride] * c;
            }
            if (d != 0)
            {
                //Aの右下ピクセル、仮にAが画像右下ピクセルだったとしても
                //そのときは面積が0のはずだからここは計算されない
                dd = pixels[ia + stride + 1] * d;
            }


            //4区を合計して四捨五入で完成
            double add = aa + bb + cc + dd;
            resultPixels[y * scaledStride + x] = (byte)(add + 0.5);

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




確認アプリのコード

github.com

作成動作環境




f:id:gogowaten:20210415132843p:plain
確認アプリWindow


MainWindow.xaml

<Window x:Class="_20210414_Bilinearで画像拡大縮小.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:_20210414_Bilinearで画像拡大縮小"
        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="MyButtonCopy" Content="コピー" Click="MyButtonCopy_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.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

//画像を拡大縮小、バイリニア法で補完、
//グレースケール専用(PixelFormats.Gray8専用)
//カラー画像をドロップした場合はグレースケール画像に変換する
//倍率は1/2、1/3、2、3
namespace _20210414_Bilinearで画像拡大縮小
{
    public partial class MainWindow : Window
    {
        private BitmapSource MyBitmapOrigin;

        public MainWindow()
        {
            InitializeComponent();
#if DEBUG
            this.Top = 0;
            this.Left = 0;
#endif

        }

        //縮小拡大対応完成版
        /// <summary>
        /// 画像の拡大縮小、バイリニア法で補完、グレースケール専用(PixelFormats.Gray8専用)
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">横ピクセル数を指定</param>
        /// <param name="tate">縦ピクセル数を指定</param>
        /// <returns></returns>
        private BitmapSource BilinearGray8専用(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];

            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点r
                    double rx = (x + 0.5) * yokoScale;
                    double ry = (y + 0.5) * tateScale;

                    //参照範囲の左上座標bp
                    double bpX = rx - 0.5;
                    //画像範囲内チェック、参照範囲が画像から外れていたら修正(収める)
                    if (bpX < 0) { bpX = 0; }
                    if (bpX > sourceWidth - 1) { bpX = sourceWidth - 1; }

                    double bpY = ry - 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 ia = ((int)bpY * stride) + (int)bpX;

                    //値*面積
                    double aa = pixels[ia] * a;
                    double bb = 0, cc = 0, dd = 0;
                    //B以降は面積が0より大きいときだけ計算
                    if (b != 0)
                    {
                        //Aの右ピクセル*Bの面積
                        bb = pixels[ia + 1] * b;
                    }
                    if (c != 0)
                    {
                        cc = pixels[ia + stride] * c;
                    }
                    if (d != 0)
                    {
                        //Aの右下ピクセル、仮にAが画像右下ピクセルだったとしても
                        //そのときは面積が0のはずだからここは計算されない
                        dd = pixels[ia + stride + 1] * d;
                    }


                    //4区を合計して四捨五入で完成
                    double add = aa + bb + cc + dd;
                    resultPixels[y * scaledStride + x] = (byte)(add + 0.5);

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


        //E:\オレ\エクセル\画像処理.xlsm_バイリニア法_$A$599
        //縮小専用完成版
        /// <summary>
        /// 画像の縮小、バイリニア法で補完、グレースケール専用(PixelFormats.Gray8専用)
        /// </summary>
        /// <param name="source">PixelFormats.Gray8のBitmap</param>
        /// <param name="yoko">変換後の横ピクセル数を指定</param>
        /// <param name="tate">変換後の縦ピクセル数を指定</param>
        /// <returns></returns>
        private BitmapSource BilinearGray8縮小専用(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];

            for (int y = 0; y < tate; y++)
            {
                for (int x = 0; x < yoko; x++)
                {
                    //参照点r
                    double rx = (x + 0.5) * yokoScale;
                    double ry = (y + 0.5) * tateScale;

                    //参照範囲の左上座標bp
                    double bpX = rx - 0.5;
                    double bpY = ry - 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 i = ((int)bpY * stride) + (int)bpX;

                    //値*面積
                    double aa = pixels[i] * a;
                    double bb = pixels[i + 1] * b;
                    double cc = pixels[i + stride] * c;
                    double dd = pixels[i + stride + 1] * d;

                    //4区を合計して四捨五入で完成
                    double add = aa + bb + cc + dd;
                    resultPixels[y * scaledStride + x] = (byte)(add + 0.5);

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


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


        //ファイルドロップ時
        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 = BilinearGray8専用(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 = BilinearGray8専用(MyBitmapOrigin, yoko, tate);
        }

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

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

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




確認

テスト用画像

f:id:gogowaten:20210415133424j:plain
3x3.bmp
小さくて見えないので30倍に拡大表示してみると
f:id:gogowaten:20210415133856p:plain
3x3.bmpを30倍に拡大表示
これはエクセルでの確認で使ったものに合わせてあるので各画素値は
f:id:gogowaten:20210415133742p:plain
画素値
こうなっている


縮小変換

2x2に変換してみる

f:id:gogowaten:20210415134500p:plain
1/2倍
3x3は1/2倍で2x2になる
3*1/2=1.5
1.5になるけど切り上げにしているので2になる
結果
f:id:gogowaten:20210415134214p:plain
2x2
拡大して確認
f:id:gogowaten:20210415135150p:plain
2x2を30倍に拡大表示
画素値は
f:id:gogowaten:20210415134851p:plain
3x3を2x2
期待通り!
エクセルで確認したときと同じ値になっている

3x3を1x1に

f:id:gogowaten:20210415140106p:plain
1x1

f:id:gogowaten:20210415140251p:plain
1x1を30倍に拡大表示
これも期待通りで、画素値は180になっている



拡大変換

同じ画像を2倍の6x6に拡大

f:id:gogowaten:20210415135704p:plain
3x3を2倍に拡大

f:id:gogowaten:20210415135838p:plain
6x6を30倍に拡大表示
いいね!

3x3を3倍の9x9

f:id:gogowaten:20210415140508p:plain
9x9

f:id:gogowaten:20210415140528p:plain
9x9を30倍に拡大表示
周縁部の2ピクセルが同じ値になっているのがイマイチな気がするけど、こうなった
これは参照範囲が画像の外側になったときの修正の仕方で変わってくるのかも、もっといい方法がありそう


写真画像で

f:id:gogowaten:20210415141156j:plain
マツバウンランの花
使う画像はサイズが1088x816の写真画像

f:id:gogowaten:20210415141400p:plain
写真画像を1/2
今回はグレースケール専用なので、カラー画像もグレースケールGray8に変換してからの変換になる

f:id:gogowaten:20210415141523p:plain
写真画像を1/2結果
元の画像によるんだろうけど、思ったよりかなりきれいに縮小された

1/3

f:id:gogowaten:20210415141741p:plain
写真画像を1/3結果
右下の日付がジャリジャリなのが気になるけど、それ以外はいいね

2倍

f:id:gogowaten:20210415142944p:plain
2倍
一部を比較、やっぱりぼやけるねえ、でも期待通り

図形画像を変換

f:id:gogowaten:20210415143437p:plain
縦縞
左から黒(0)、白(255)と交互に並んだ32x32の画像
前回はこの画像を変換したときになんか違うなって思った、今回は?

2倍に拡大

f:id:gogowaten:20210415143751p:plain
縦縞を2倍
見た感じは期待通りになった、あっているかはわからないけど前回より格段に良くなった、もうこれで正解でしょ

1/2倍

f:id:gogowaten:20210415144251p:plain
縦縞を1/2倍
これもいいね、画素値はすべて128、こういうのでいいんだよ


リサイズアプリと比較

藤 -Resizer- 3.8.0.231 64bit版

f:id:gogowaten:20210415162624p:plain
藤 -Resizer- 3.8.0.231 64bit版の設定
Ralpha Image Resizer 170111
f:id:gogowaten:20210415162915p:plain
Ralpha Image Resizer 170111の設定

f:id:gogowaten:20210415162310p:plain
縦縞を1/2にして比較
縦縞を1/2
テストアプリの結果は藤の平均画素法ってのと同じ結果になった
Ralphaは両端の処理が違うみたいで全く違う値になっている、全体も127と1小さい値になった

2倍に拡大

f:id:gogowaten:20210415161429p:plain
縦縞を2倍に拡大で比較、左端

f:id:gogowaten:20210415161433p:plain
縦縞を2倍に拡大で比較、右端

今度は逆にRalphaと同じになった
そして藤はずいぶん違う結果になった、藤の平均画素法ってのはバイリニアとは違うのかもしれない、それにしてもぜんぜん違うなあ

エクセルで使った値の画像

f:id:gogowaten:20210415170712p:plain
3x3.bmpで比較
1/3倍で1ピクセルは全員180になった!
2倍に拡大はまた藤だけ違っていて、四隅の値を見ると元画像の画素値を超えた値になっている、名前も違うからバイリニア法じゃないみたいねえ

f:id:gogowaten:20210415171957p:plain
30倍に拡大表示して画素値を表示するのに使った



感想

バイリニア法とかで検索すると数式が出てくるけど、それを実際にどう使えばいいのかとか僕のアタマじゃわかんない、なのでエクセル方眼紙で画素値を入れて色塗って、この座標ならこのピクセルの画素値はだいたいこうなるはずだから、そうするにはどうすればいいのかなってこねくり回してできたと思ったのが3回くらいあったかな、ようやくできた

バイキュービック法かランチョス法のまえに、それよりもかんたんなバイリニア法を納得できるようにしておきたかったんだよねえ、3年かかったけど満足
他のアプリと比較するとことで今回の方法があっているの確認したかったけど、よくわからん、でも正解じゃないとしても、かなり近いんじゃないかなあと思うし、何より前回は違和感があった縦縞の変換が、期待していたとおりの結果になったのがいい、とてもいい

次はカラーとマルチスレッドに対応させて、それからバイキュービック法かランチョス法


関連記事

縮小処理を書き直したのは3週間後
gogowaten.hatenablog.com 拡大処理は良かっただけどねえ



完成版は明後日
gogowaten.hatenablog.com

次回は明日
gogowaten.hatenablog.com

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

前回のバイリニア法は3年前
gogowaten.hatenablog.com

冒頭にある

だいたいあっていると思う

これはフラグだったんだなあ

再近傍補完法で納得できたのは2年前
gogowaten.hatenablog.com
再近傍補完法って呼び方なら、バイリニア法はバイリニア補完法のほうがいいのかな

リサイズアプリのウィンドウキャプチャに使ったアプリは43日前
gogowaten.hatenablog.com