午後わてんのブログ

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

最近傍補間法で画像の拡大縮小試してみた、2回め

ようやく納得できるまでになった

 

変換後の座標をxとして、xが参照するべき変換前座標は

(x + 0.5) * (変換前ピクセル数 / 変換後ピクセル数)

これの小数点以下切り捨てた値

 

例えば倍率が3で、変換前画像の横ピクセル数が4のときは

変換後画像の横ピクセル数は3*4=12になる

これをもとに変換後座標5が参照するべき変換前座標は

(5 + 0.5) * (4 / 12) = 1.8333…

1.8333…の小数点以下切り捨てて

答えは1

変換後座標の5の色は、変換前座標1の色にする

 

f:id:gogowaten:20191108132958p:plain

ここまでは横方向xだけの場合だけど、縦方向yが入ってきても、それぞれを計算すればいいだけ

 

ここに至るまで

変換後の画像サイズは四捨五入

ピクセル数が2、倍率が1.9のとき、計算すると2 * 1.9 = 3.8

小数点以下を切り捨てると3、四捨五入なら4

3.8から丸めるなら四捨五入のほうが自然だと感じるので、これは四捨五入でいいと思う

 

エクセル方眼紙で確認しながら

f:id:gogowaten:20191108135636p:plain

理想はこうね、期待する変換

まず横方向だけで考えてみて

 

f:id:gogowaten:20191108135027p:plain

3ピクセルを3倍して9ピクセルにするとき

変換後座標に逆倍率を掛け算した値を四捨五入していた、逆倍率ってのは変換後から見た変換前の倍率の事を言っている、この場合だと3/9=0.333…

そんで四捨五入だと変換前座標0を参照するピクセルが2個しかなくて不自然になるし、右端は存在しない座標を参照してしまうので修正が必要、修正して一番近いとなりの2を参照すると、2を参照するピクセルが4つになって、更に不自然

逆に切り捨て方式だと期待通りになった!

と、ここまでが前回の方法、1年7ヶ月前

gogowaten.hatenablog.com

でも、なんか違和感があった

 

 

縮小の場合

9ピクセルを1/3倍して3ピクセルにする

f:id:gogowaten:20191108140803p:plain

理想はこう、変換後の0は変換前1を参照、1は4、2は7を参照

これをさっきの方法で行うと

 

f:id:gogowaten:20191108141028p:plain

理想の参照先とは左に1ずれているけど、結果的に色はあっている

結果はあっているけど、なんか違う

 

 

ピクセルにも幅はある

f:id:gogowaten:20191108134043p:plain

見た目的にも幅があるってのはわかるけど、計算するときは、左端なら0で計算、その右隣は1で計算ってしていた、でも幅があるならその値だとピクセルの左端で計算していることになるので、これが違うのかなと思って、だったらピクセルの中心の座標で計算したらどうなるのかなと

 

 

f:id:gogowaten:20191108142949p:plain

いいと思う

変換後の中心座標の4ピクセルの中心座標は4.5で計算すると1.5、これは変換前の中心座標の1ピクセルの中心座標1.5と同じ、他のピクセルも0.5を足した値で計算したほうが自然な位置になっていると思う

 

中心座標と切り捨てで計算してみる

f:id:gogowaten:20191108143745p:plain

期待通りの値になった

 

 

f:id:gogowaten:20191108144345p:plain

 

f:id:gogowaten:20191108144551p:plain



 

f:id:gogowaten:20191108144836p:plain

9ピクセルを1/3倍も

f:id:gogowaten:20191108140803p:plain
この理想と同じになった!

 

 

4を1.5倍の6ピクセルは 

f:id:gogowaten:20191108151924p:plain

0,1,1,2,3,3になった 

 

グーグルスプレッドシートで作ってみた

docs.google.com

 

 

 

確認用アプリ

wpf_test2/20191105_最近傍補間法.zip

github.com

f:id:gogowaten:20191108161905p:plain

paste:クリップボードにある画像を追加

reset:元の画像サイズに戻す

数値ボタン:表示している画像を拡大する

copy:表示している画像をクリップボードにコピーする

x16(確認用):表示している画像を16倍して表示する(ピクセル数が1000以下の画像だけ)

 

 

f:id:gogowaten:20191108163252p:plain

3x1の画像を貼り付けたところ

 

小さくてわからないので16倍に拡大して確認

f:id:gogowaten:20191108163333p:plain

こういう画像

 

resetでもとに戻してから

f:id:gogowaten:20191108163521p:plain

3倍に拡大したところ

 

16倍に拡大して確認

f:id:gogowaten:20191108163623p:plain

期待通りにできてる

 

f:id:gogowaten:20191108164329p:plain

エクセルのウィンドウの左上部分を

 

2倍

f:id:gogowaten:20191108164531p:plain


1.5倍

f:id:gogowaten:20191108164626p:plain

 

2x2で4倍

f:id:gogowaten:20191108165021p:plain

 

1/2倍

f:id:gogowaten:20191108164646p:plain

 

 

他のアプリと比較してみる

f:id:gogowaten:20191108165511p:plain

paint.netに画像を貼り付けた画像を、2倍に拡大しようとしているところ

イメージ→サイズ変更で、再サンプリングを直近、これが最近傍補間法だと思う、後は%で指定にチェック入れて数値を200でOK

 

f:id:gogowaten:20191108165816p:plain

拡大された、見た目だけなら全くおなじに見えるけど実際にはどうなのか、前回作った画像比較アプリを使って確かめてみる

gogowaten.hatenablog.com

 

 

f:id:gogowaten:20191108170343p:plain

左がpaint.netから貼り付けた画像、右が今回の確認用アプリから貼り付けた画像

結果は同じ!

 

1.5倍で比較

f:id:gogowaten:20191108170753p:plain

これも全く同じ結果になった!

1/2倍

f:id:gogowaten:20191108171014p:plain

縮小でも同じ、ってことでpaint.netとは同じ

 

 

f:id:gogowaten:20191108171507p:plain

ペイントで1.5倍、これをコピーして比較アプリに貼り付けたんだけど表示されない

f:id:gogowaten:20191108171642p:plain

左に貼り付けたんだけど見えない、これはアルファの値が0になってしまうWPFクリップボードの不具合というか仕様かも

なので別のアプリに貼り付けたのをコピーして貼り付けて比較したら

f:id:gogowaten:20191108172209p:plain

これも同じになった

 

 

f:id:gogowaten:20191108172545p:plain

JTrimで1.5倍、これもコピペできなかったので別のアプリ経由で貼り付けて

比較

f:id:gogowaten:20191108172813p:plain

同じ!ってことで計算方法ともかく最近傍補間法での拡大は正しくできているみたい

 

 

 

f:id:gogowaten:20191108173157p:plain

黒と白のしましま画像、左端が黒から始まるこれを1/2倍

f:id:gogowaten:20191108173526p:plain

黒がなくなって真っ白になった、最近傍補間法は四捨五入っていうからこれが正しいはず、前回は逆に黒になっていた

 

 

 

<Window x:Class="_20191105_最近傍補間法.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:_20191105_最近傍補間法"
        mc:Ignorable="d"
        Title="MainWindow" Height="450" Width="800">
    <Grid>
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="100"/>
      <ColumnDefinition/>
    </Grid.ColumnDefinitions>
    <StackPanel Grid.Column="0">
      <Button Content="paste" Click="ButtonPaste_Click"/>
      <Button Content="reset" Click="ButtonReset_Click"/>
      <Button Content="x2" Click="ButtonUp_Click" Tag="2"/>
      <Button Content="x3" Click="ButtonUp_Click" Tag="3"/>
      <Button Content="x1.5" Click="ButtonUp_Click" Tag="1.5"/>
      <Button Content="1/2" Click="ButtonDown_Click_1" Tag="2"/>
      <Button Content="1/3" Click="ButtonDown_Click_1" Tag="3"/>
      <Button Content="1/2 ScaleTransform" Click="ButtonDownScaleTransform_Click" Tag="2"/>
      <Button Content="copy" Click="ButtonCopy_Click"/>
      <Button Content="x16(確認用)" Click="Button16_Click"/>
    </StackPanel>
    <ScrollViewer Grid.Column="1" UseLayoutRounding="True" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
      <Image Name="MyImage" Stretch="None" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="10"/>
    </ScrollViewer>
    </Grid>
</Window>

 

 

using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace _20191105_最近傍補間法
{
    /// <summary>
    /// MainWindow.xaml の相互作用ロジック
    /// </summary>
    public partial class MainWindow : Window
    {
        private BitmapSource Source;
        public MainWindow()
        {
            InitializeComponent();
        }


        /// <summary>
        /// 最近傍補間法で画像拡大、対応ピクセルフォーマットはBgra32、他にはBgr32、Pbgra32もできるはず
        /// </summary>
        /// <param name="source"></param>
        /// <param name="scale">拡大倍率</param>
        /// <returns></returns>
        private BitmapSource NearestNeighbor(BitmapSource source, double scale)
        {
            //変換前画像のCopyPixels作成
            int w = source.PixelWidth;
            int h = source.PixelHeight;
            int stride = w * 4;
            byte[] pixels = new byte[h * stride];
            source.CopyPixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);

            //変換後ピクセル数は四捨五入
            int ww = (int)Math.Round(w * scale, MidpointRounding.AwayFromZero);
            int hh = (int)Math.Round(h * scale, MidpointRounding.AwayFromZero);
            if (ww == 0 || hh == 0)
            {
                return source;
            }
            int sstride = ww * 4;
            byte[] ppixels = new byte[hh * sstride];

            double rScale = (double)w / ww;//逆倍率

            //変換後の座標から参照するべき変換前の座標を計算
            //変換前座標 = 切り捨て(変換後座標+0.5)*逆倍率)
            for (int y = 0; y < hh; y++)
            {
                for (int x = 0; x < ww; x++)
                {
                    int pp = y * sstride + x * 4;//変換後の座標
                    int ny = (int)((y + 0.5) * rScale);//intへのキャストで小数点以下切り捨て
                    int nx = (int)((x + 0.5) * rScale);
                    int p = (ny * stride) + (nx * 4);//変換前の座標

                    ppixels[pp] = pixels[p];
                    ppixels[pp + 1] = pixels[p + 1];
                    ppixels[pp + 2] = pixels[p + 2];
                    ppixels[pp + 3] = pixels[p + 3];
                }
            }
            return BitmapSource.Create(ww, hh, 96, 96, source.Format, null, ppixels, sstride);
        }

        private void ButtonUp_Click(object sender, RoutedEventArgs e)
        {
            var source = (BitmapSource)MyImage.Source;
            if (source == null) return;
            var s = (Button)sender;
            var scale = double.Parse(s.Tag.ToString());
            MyImage.Source = NearestNeighbor(source, scale);
        }

        private void ButtonDown_Click_1(object sender, RoutedEventArgs e)
        {
            var source = (BitmapSource)MyImage.Source;
            if (source == null) return;
            var s = (Button)sender;
            var scale = 1 / double.Parse(s.Tag.ToString());
            MyImage.Source = NearestNeighbor(source, scale);
        }




        private void ButtonReset_Click(object sender, RoutedEventArgs e)
        {
            MyImage.Source = Source;
            MyImage.RenderTransform = new ScaleTransform(1, 1);
        }

        private void ButtonPaste_Click(object sender, RoutedEventArgs e)
        {
            var bmp = Clipboard.GetImage();
            if (bmp == null) return;
            Source = new FormatConvertedBitmap(bmp, PixelFormats.Bgra32, null, 0);
            MyImage.Source = Source;
        }

        private void ButtonDownScaleTransform_Click(object sender, RoutedEventArgs e)
        {
            var s = (Button)sender;
            var scale = 1 / double.Parse(s.Tag.ToString());
            MyImage.RenderTransform = new ScaleTransform(scale, scale);
            RenderOptions.SetBitmapScalingMode(MyImage, BitmapScalingMode.NearestNeighbor);
        }

        private void ButtonCopy_Click(object sender, RoutedEventArgs e)
        {
            var source = MyImage.Source as BitmapSource;
            if (source == null) return;
            Clipboard.SetImage(source);
        }

        //16倍に拡大
        private void Button16_Click(object sender, RoutedEventArgs e)
        {
            int scale = 16;
            int limit = 1000;//このピクセル数を超える画像は処理しない
            BitmapSource source = (BitmapSource)MyImage.Source;
            int w = source.PixelWidth;
            int h = source.PixelHeight;
            if (w * h > limit)
            {
                return;
            }
            int stride = w * 4;
            byte[] pixels = new byte[h * stride];
            source.CopyPixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);

            int ww = w * scale;
            int hh = h * scale;
            if (ww == 0 || hh == 0)
            {
                MyImage.Source = source;
            }

            int sstride = ww * 4;
            byte[] ppixels = new byte[hh * sstride];

            int p, pp;
            for (int y = 0; y < h; y++)
            {
                for (int x = 0; x < w; x++)
                {
                    p = y * stride + x * 4;
                    var yy = y * scale;
                    var xx = x * scale;

                    for (int i = 0; i < scale; i++)
                    {
                        for (int j = 0; j < scale; j++)
                        {
                            pp = ((yy + i) * sstride) + ((xx + j) * 4);
                            ppixels[pp] = pixels[p];
                            ppixels[pp + 1] = pixels[p + 1];
                            ppixels[pp + 2] = pixels[p + 2];
                            ppixels[pp + 3] = pixels[p + 3];
                        }
                    }
                }
            }
            MyImage.Source = BitmapSource.Create(ww, hh, 96, 96, source.Format, null, ppixels, sstride);
        }
    }
}

//E:\オレ\エクセル\画像処理.xlsm_最近傍法_$A$460

 

最近傍補間法での画像拡大は27~66行目

/// <summary>
/// 最近傍補間法で画像拡大、対応ピクセルフォーマットはBgra32、他にはBgr32、Pbgra32もできるはず
/// </summary>
/// <param name="source"></param>
/// <param name="scale">拡大倍率</param>
/// <returns></returns>
private BitmapSource NearestNeighbor(BitmapSource source, double scale)
{
    //変換前画像のCopyPixels作成
    int w = source.PixelWidth;
    int h = source.PixelHeight;
    int stride = w * 4;
    byte[] pixels = new byte[h * stride];
    source.CopyPixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);

    //変換後ピクセル数は四捨五入
    int ww = (int)Math.Round(w * scale, MidpointRounding.AwayFromZero);
    int hh = (int)Math.Round(h * scale, MidpointRounding.AwayFromZero);
    if (ww == 0 || hh == 0)
    {
        return source;
    }
    int sstride = ww * 4;
    byte[] ppixels = new byte[hh * sstride];

    double rScale = (double)w / ww;//逆倍率

    //変換後の座標から参照するべき変換前の座標を計算
    //変換前座標 = 切り捨て(変換後座標+0.5)*逆倍率)
    for (int y = 0; y < hh; y++)
    {
        for (int x = 0; x < ww; x++)
        {
            int pp = y * sstride + x * 4;//変換後の座標
            int ny = (int)((y + 0.5) * rScale);//intへのキャストで小数点以下切り捨て
            int nx = (int)((x + 0.5) * rScale);
            int p = (ny * stride) + (nx * 4);//変換前の座標

            ppixels[pp] = pixels[p];
            ppixels[pp + 1] = pixels[p + 1];
            ppixels[pp + 2] = pixels[p + 2];
            ppixels[pp + 3] = pixels[p + 3];
        }
    }
    return BitmapSource.Create(ww, hh, 96, 96, source.Format, null, ppixels, sstride);
}

前回と違うのは0.5を足していることだけ

 

 

16倍に拡大

//16倍に拡大
private void Button16_Click(object sender, RoutedEventArgs e)
{
    int scale = 16;
    int limit = 1000;//このピクセル数を超える画像は処理しない
    BitmapSource source = (BitmapSource)MyImage.Source;
    int w = source.PixelWidth;
    int h = source.PixelHeight;
    if (w * h > limit)
    {
        return;
    }
    int stride = w * 4;
    byte[] pixels = new byte[h * stride];
    source.CopyPixels(new Int32Rect(0, 0, w, h), pixels, stride, 0);

    int ww = w * scale;
    int hh = h * scale;
    if (ww == 0 || hh == 0)
    {
        MyImage.Source = source;
    }

    int sstride = ww * 4;
    byte[] ppixels = new byte[hh * sstride];

    int p, pp;
    for (int y = 0; y < h; y++)
    {
        for (int x = 0; x < w; x++)
        {
            p = y * stride + x * 4;
            var yy = y * scale;
            var xx = x * scale;

            for (int i = 0; i < scale; i++)
            {
                for (int j = 0; j < scale; j++)
                {
                    pp = ((yy + i) * sstride) + ((xx + j) * 4);
                    ppixels[pp] = pixels[p];
                    ppixels[pp + 1] = pixels[p + 1];
                    ppixels[pp + 2] = pixels[p + 2];
                    ppixels[pp + 3] = pixels[p + 3];
                }
            }
        }
    }
    MyImage.Source = BitmapSource.Create(ww, hh, 96, 96, source.Format, null, ppixels, sstride);
}

整数での拡大なら最近傍補間法と結果は同じ、計算速度は小数点を使わないこっちのほうが速いかも?

 

 

 

 

最近傍補間法でググった先だと四捨五入で計算するってあるんだけど、実際にどう計算して結果はこうなる、みたいなのがなくてわかんないんだよねえ、そのまま計算してみたらなんか違う気がしていて、今回ようやく納得行く結果になったんだけど、四捨五入じゃなくて切り捨てているから本当の最近傍じゃないのかもしれない、でも四捨五入の方法に0.5足してから切り捨てるって方法もあるから、座標に0.5足しているのは四捨五入っぽくもある

わからずじまいだけど、納得できたのでとても気分がいい

 

 

関連記事

2019/11/12は4日後

gogowaten.hatenablog.com

これで透明画像にならずに取得できるようになった

 

 

2年後、バイリニア法での2回め

gogowaten.hatenablog.com