ようやく納得できるまでになった
変換後の座標をxとして、xが参照するべき変換前座標は
(x + 0.5) * (変換前ピクセル数 / 変換後ピクセル数)
これの小数点以下切り捨てた値
例えば倍率が3で、変換前画像の横ピクセル数が4のときは
変換後画像の横ピクセル数は3*4=12になる
これをもとに変換後座標5が参照するべき変換前座標は
(5 + 0.5) * (4 / 12) = 1.8333…
1.8333…の小数点以下切り捨てて
答えは1
変換後座標の5の色は、変換前座標1の色にする
ここまでは横方向xだけの場合だけど、縦方向yが入ってきても、それぞれを計算すればいいだけ
ここに至るまで
変換後の画像サイズは四捨五入
横ピクセル数が2、倍率が1.9のとき、計算すると2 * 1.9 = 3.8
小数点以下を切り捨てると3、四捨五入なら4
3.8から丸めるなら四捨五入のほうが自然だと感じるので、これは四捨五入でいいと思う
エクセル方眼紙で確認しながら
理想はこうね、期待する変換
まず横方向だけで考えてみて
3ピクセルを3倍して9ピクセルにするとき
変換後座標に逆倍率を掛け算した値を四捨五入していた、逆倍率ってのは変換後から見た変換前の倍率の事を言っている、この場合だと3/9=0.333…
そんで四捨五入だと変換前座標0を参照するピクセルが2個しかなくて不自然になるし、右端は存在しない座標を参照してしまうので修正が必要、修正して一番近いとなりの2を参照すると、2を参照するピクセルが4つになって、更に不自然
逆に切り捨て方式だと期待通りになった!
と、ここまでが前回の方法、1年7ヶ月前
gogowaten.hatenablog.com
でも、なんか違和感があった
縮小の場合
9ピクセルを1/3倍して3ピクセルにする
理想はこう、変換後の0は変換前1を参照、1は4、2は7を参照
これをさっきの方法で行うと
理想の参照先とは左に1ずれているけど、結果的に色はあっている
結果はあっているけど、なんか違う
見た目的にも幅があるってのはわかるけど、計算するときは、左端なら0で計算、その右隣は1で計算ってしていた、でも幅があるならその値だとピクセルの左端で計算していることになるので、これが違うのかなと思って、だったらピクセルの中心の座標で計算したらどうなるのかなと
いいと思う
変換後の中心座標の4ピクセルの中心座標は4.5で計算すると1.5、これは変換前の中心座標の1ピクセルの中心座標1.5と同じ、他のピクセルも0.5を足した値で計算したほうが自然な位置になっていると思う
中心座標と切り捨てで計算してみる
期待通りの値になった
9ピクセルを1/3倍も
この理想と同じになった!
4を1.5倍の6ピクセルは
0,1,1,2,3,3になった
グーグルスプレッドシートで作ってみた
docs.google.com
確認用アプリ
wpf_test2/20191105_最近傍補間法.zip
github.com
paste:クリップボードにある画像を追加
reset:元の画像サイズに戻す
数値ボタン:表示している画像を拡大する
copy:表示している画像をクリップボードにコピーする
x16(確認用):表示している画像を16倍して表示する(ピクセル数が1000以下の画像だけ)
3x1の画像を貼り付けたところ
小さくてわからないので16倍に拡大して確認
こういう画像
resetでもとに戻してから
3倍に拡大したところ
16倍に拡大して確認
期待通りにできてる
エクセルのウィンドウの左上部分を
2倍
1.5倍
2x2で4倍
1/2倍
他のアプリと比較してみる
paint.netに画像を貼り付けた画像を、2倍に拡大しようとしているところ
イメージ→サイズ変更で、再サンプリングを直近、これが最近傍補間法だと思う、後は%で指定にチェック入れて数値を200でOK
拡大された、見た目だけなら全くおなじに見えるけど実際にはどうなのか、前回作った画像比較アプリを使って確かめてみる
gogowaten.hatenablog.com
左がpaint.netから貼り付けた画像、右が今回の確認用アプリから貼り付けた画像
結果は同じ!
1.5倍で比較
これも全く同じ結果になった!
1/2倍
縮小でも同じ、ってことでpaint.netとは同じ
ペイントで1.5倍、これをコピーして比較アプリに貼り付けたんだけど表示されない
左に貼り付けたんだけど見えない、これはアルファの値が0になってしまうWPFのクリップボードの不具合というか仕様かも
なので別のアプリに貼り付けたのをコピーして貼り付けて比較したら
これも同じになった
JTrimで1.5倍、これもコピペできなかったので別のアプリ経由で貼り付けて
比較
同じ!ってことで計算方法ともかく最近傍補間法での拡大は正しくできているみたい
黒と白のしましま画像、左端が黒から始まるこれを1/2倍
黒がなくなって真っ白になった、最近傍補間法は四捨五入っていうからこれが正しいはず、前回は逆に黒になっていた
<Window xClass="_20191105_最近傍補間法.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlnsx="http://schemas.microsoft.com/winfx/2006/xaml"
xmlnsd="http://schemas.microsoft.com/expression/blend/2008"
xmlnsmc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlnslocal="clr-namespace:_20191105_最近傍補間法"
mcIgnorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<GridColumnDefinitions>
<ColumnDefinition Width="100"/>
<ColumnDefinition/>
</GridColumnDefinitions>
<StackPanel GridColumn="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 GridColumn="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>
</summary>
public partial class MainWindow : Window
{
private BitmapSource Source;
public MainWindow()
{
InitializeComponent();
}
<summary>
</summary>
<param name="source"></param>
<param name="scale"></param>
<returns></returns>
private BitmapSource NearestNeighbor(BitmapSource source, double scale)
{
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;
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 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);
}
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);
}
}
}
最近傍補間法での画像拡大は27~66行目
<summary>
</summary>
<param name="source"></param>
<param name="scale"></param>
<returns></returns>
private BitmapSource NearestNeighbor(BitmapSource source, double scale)
{
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;
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 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倍に拡大
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