前回のランチョスでの画像リサイズは、拡大はあっていたけど縮小処理が間違っていた
今回のは全体的に少しぼやけているけど、途切れていた電線がつながった
解決まで
なんで斜めの線が点線になっていたのか
1/5倍だと参照点同士の間隔は5ピクセルになる
1/10倍だと10ピクセル間隔、小さくするほど感覚が開いていくので、参照先で先に当たる確率が下がってより点線になってしまう
参照点から参照範囲
参照距離n=1
参照点からの参照距離を1にすると、縮小後画像の左から3番目(2,0)が参照するピクセルは真っ白、これでは点線になってしまう
参照距離を延ばしてn=3
これで参照範囲が広がって参照点から遠いピクセルの色も考慮されるんだけど、重みが違う、重みのほとんどは参照点付近に集中しているので、範囲を広げても遠いピクセルの色の影響は薄いし、距離が1~2の間はマイナスの重みになるので、やっぱり白になってしまう
解決するには
- 重みの分散、0付近に集中している重みをもう少し遠くまで分ける
- 倍率によって分散具合を調節する
小さくするほど参照点は過疎るのでより分散させたい、通常のランチョスだと距離0から離れていって、重みが0になるのが距離1のときなので、これを目安にして、倍率が1/2なら重みが0になる距離を2、倍率が1/5なら重みが0になる距離を5とかにすれば良さそう、こうするには距離を短く見せかければいいので距離dに倍率を掛け算した値をランチョスの計算式に渡してみたら
できたっぽい、1/5倍で重みが0になる距離は5、1/3倍は3になっている
sinc(d) * sinc(d / n)だったのを
sinc(d * 倍率) * sinc(d * 倍率 / n)に変えただけ
指定距離(n)と実際に参照する距離
倍率1/3でn=2、n=3、n=4のときは
倍率1/3でn=2で、そのまま距離2までを参照距離とすると全然足りなくて、実際に使いたい距離(2回めの重み0)は6のところになる、同様にn=3だと9まで使いたいし、n=4なら12
これの計算は指定したnに逆倍率を掛け算で良さそう
逆倍率ってのは1を倍率で割り算した値のことを言っている、名前がありそうだけどわからん
まとめると
計算式は
sinc(d * 倍率) * sinc(d * 倍率 / n)
重み計算するときは指定nを使うけど、実際に参照する距離はn*逆倍率ってことにした
コード
//窓関数 private double Sinc(double d) { return Math.Sin(Math.PI * d) / (Math.PI * d); } /// <summary> /// 縮小拡大両対応重み計算、Lanczos /// </summary> /// <param name="d">距離</param> /// <param name="n">最大参照距離</param> /// <param name="scale">倍率</param> /// <returns></returns> private double GetLanczosWeight(double d, int n, double limitD) { if (d == 0) return 1.0; else if (d > limitD) return 0.0; else return Sinc(d) * Sinc(d / n); } //セパラブルといっても全く同一じゃない、少し誤差が出るみたい //正確さを求めるなら使わないほうがいい /// <summary> /// 画像の縮小、ランチョス法で補完、PixelFormats.Bgra32専用) /// 通常版をセパラブルとParallelで高速化 /// </summary> /// <param name="source">PixelFormats.Bgra32のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgra32Ex(BitmapSource source, int width, int height, int n) { //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]; //倍率 double scale = width / (double)sourceWidth; //最大参照距離 = 逆倍率 * n double limitD = widthScale * n; //ピクセルでの最大参照距離、は指定距離*逆倍率の切り上げにした int actD = (int)Math.Ceiling(limitD); //拡大時の調整(これがないと縮小専用) if (1.0 < scale) { scale = 1.0;//重み計算に使うようで、拡大時は1固定 actD = n;//拡大時の実際の参照距離は指定距離と同じ } //横処理 _ = 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[] ws = GetFixWeihgts(rx, n, actD, scale, limitD); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; int pp; for (int xx = -actD; xx < actD; xx++) { int xc = xKijun + xx; //マイナス座標や画像サイズを超えていたら、収まるように修正 xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc; pp = (y * sourceStride) + (xc * pByte); double weight = ws[xx + actD]; //完全透明ピクセル(a=0)だった場合はRGBは計算しないで //重みだけ足し算して後で使う if (sourcePixels[pp + 3] == 0) { alphaFix += weight; continue; } bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; aSum += 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値の修正 //参照範囲がすべて完全透明だった場合は0のままでいいので計算しない if (alphaFix == 1) continue; //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する double rgbFix = 1 / (1 - alphaFix); bSum *= rgbFix; gSum *= rgbFix; rSum *= rgbFix; pp = y * stride + x * pByte; xResult[pp] = bSum; xResult[pp + 1] = gSum; xResult[pp + 2] = rSum; xResult[pp + 3] = aSum; } }); //縦処理 _ = 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[] ws = GetFixWeihgts(ry, n, actD, scale, limitD); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; int pp; for (int yy = -actD; yy < actD; yy++) { int yc = yKijun + yy; yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; pp = (yc * stride) + (x * pByte); double weight = ws[yy + actD]; if (xResult[pp + 3] == 0) { alphaFix += weight; continue; } bSum += xResult[pp] * weight; gSum += xResult[pp + 1] * weight; rSum += xResult[pp + 2] * weight; aSum += xResult[pp + 3] * weight; } if (alphaFix == 1) continue; //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する double rgbFix = 1 / (1 - alphaFix); bSum *= rgbFix; gSum *= rgbFix; rSum *= rgbFix; //0~255の範囲を超えることがあるので、修正 bSum = bSum < 0 ? 0 : bSum > 255 ? 255 : bSum; gSum = gSum < 0 ? 0 : gSum > 255 ? 255 : gSum; rSum = rSum < 0 ? 0 : rSum > 255 ? 255 : rSum; aSum = aSum < 0 ? 0 : aSum > 255 ? 255 : aSum; int ap = (y * stride) + (x * pByte); pixels[ap] = (byte)(bSum + 0.5); pixels[ap + 1] = (byte)(gSum + 0.5); pixels[ap + 2] = (byte)(rSum + 0.5); pixels[ap + 3] = (byte)(aSum + 0.5); } }); BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[] GetFixWeihgts(double r, int n, int actD, double scale, double limitD) { int nn = actD * 2;//全体の参照距離 //基準距離 double s = r - (int)r; double d = (s < 0.5) ? 0.5 - s : 0.5 - s + 1; //各重みと重み合計 double[] ws = new double[nn]; double sum = 0; for (int i = -actD; i < actD; i++) { double w = GetLanczosWeight(Math.Abs(d + i) * scale, n, limitD); sum += w; ws[i + actD] = w; } //重み合計で割り算して修正、全体で100%(1.0)にする for (int i = 0; i < nn; i++) { ws[i] /= sum; } return ws; } } /// <summary> /// 画像のリサイズ、ランチョス法で補完、PixelFormats.Bgra32専用) /// 高速化なし /// </summary> /// <param name="source">PixelFormats.Bgra32のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgra32(BitmapSource source, int width, int height, int n) { //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 scale = width / (double)sourceWidth; //最大参照距離 = 逆倍率 * n double limitD = widthScale * n; //実際の参照距離、は指定距離*逆倍率の切り上げにしたけど、切り捨てでも見た目の変化なし int actD = (int)Math.Ceiling(limitD); //int actD = (int)(limitD); //拡大時の調整(これがないと縮小専用) if (1.0 < scale) { scale = 1.0;//重み計算に使うようで、拡大時は1固定 actD = n;//拡大時の実際の参照距離は指定距離と同じ } 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[,] ws = GetFixWeights(rx, ry, actN, scale*0.1, n); double[,] ws = GetFixWeights(rx, ry, actD, scale, n, limitD); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; //参照範囲は基準から上(xは左)へnn、下(xは右)へnn-1の範囲 for (int yy = -actD; yy < actD; yy++) { int yc = yKijun + yy; //マイナス座標や画像サイズを超えていたら、収まるように修正 yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; for (int xx = -actD; xx < actD; xx++) { int xc = xKijun + xx; xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc; int pp = (yc * sourceStride) + (xc * pByte); double weight = ws[xx + actD, yy + actD]; //完全透明ピクセル(a=0)だった場合はRGBは計算しないで //重みだけ足し算して後で使う if (sourcePixels[pp + 3] == 0) { alphaFix += weight; continue; } bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; aSum += 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値の修正 //参照範囲がすべて完全透明だった場合は0のままでいいので計算しない if (alphaFix == 1) continue; //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する double rgbFix = 1 / (1 - alphaFix); bSum *= rgbFix; gSum *= rgbFix; rSum *= rgbFix; //0~255の範囲を超えることがあるので、修正 bSum = bSum < 0 ? 0 : bSum > 255 ? 255 : bSum; gSum = gSum < 0 ? 0 : gSum > 255 ? 255 : gSum; rSum = rSum < 0 ? 0 : rSum > 255 ? 255 : rSum; aSum = aSum < 0 ? 0 : aSum > 255 ? 255 : aSum; int ap = (y * stride) + (x * pByte); pixels[ap] = (byte)(bSum + 0.5); pixels[ap + 1] = (byte)(gSum + 0.5); pixels[ap + 2] = (byte)(rSum + 0.5); pixels[ap + 3] = (byte)(aSum + 0.5); } } //_ = Parallel.For(0, height, y => // { // }); BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[,] GetFixWeights(double rx, double ry, int actN, double scale, int n, double limitD) { //全体の参照距離 int nn = actN * 2; //基準になる距離計算 double sx = rx - (int)rx; double sy = ry - (int)ry; double dx = (sx < 0.5) ? 0.5 - sx : 0.5 - sx + 1; double dy = (sy < 0.5) ? 0.5 - sy : 0.5 - sy + 1; //各ピクセルの重みと、重み合計を計算 double[] xw = new double[nn]; double[] yw = new double[nn]; double xSum = 0, ySum = 0; for (int i = -actN; i < actN; i++) { //距離に倍率を掛け算したのをLanczosで重み計算 double x = GetLanczosWeight(Math.Abs(dx + i) * scale, n, limitD); xSum += x; xw[i + actN] = x; double y = GetLanczosWeight(Math.Abs(dy + i) * scale, n, limitD); ySum += y; yw[i + actN] = y; } //重み合計で割り算して修正、全体で100%(1.0)にする for (int i = 0; i < nn; i++) { xw[i] /= xSum; yw[i] /= ySum; } // x * y double[,] ws = new double[nn, nn]; for (int y = 0; y < nn; y++) { for (int x = 0; x < nn; x++) { ws[x, y] = xw[x] * yw[y]; } } return ws; } }
LanczosBgra32Exは速度重視
LanczosBgra32は精度重視
精度は誤差程度なので普通はExのほうでいい
テストアプリ
- 画像ファイルドロップかクリップボードからの貼り付けで画像表示
- 倍率は1~10
- 倍率=2で縮小は1/2、倍率=5で縮小は1/5
- セパは縦横別々計算、処理は速いけど誤差が出る
- n=3が標準、大きくすると処理時間かかる
- 左下に処理時間表示
- 処理中は操作不能
- 空きメモリが2GB以下と少ないときに、大きな画像(1000x1000以上)を10倍拡大処理するとアプリが落ちる
- 表示倍率は1~4、ニアレストネイバーで表示だけ拡大する、輪郭がどれくらい出ているか確認用
作成動作環境
- Windows 10 Home バージョン 2004
- Visual Studio Community 2019
- WPF
- C#
- .NET 5
.NET 5以上が必要
ダウンロード
ここの20210505_Lanczos.zip
ここの20210505_Lanczos.1.0.1.zip
github.com
コード
MainWindow.xaml
<Window x:Class="_20210505_Lanczosで縮小.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:_20210505_Lanczosで縮小" mc:Ignorable="d" Title="MainWindow" Height="587" Width="634" AllowDrop="True" Drop="Window_Drop"> <Window.Resources> <Style TargetType="Button"> <Setter Property="Margin" Value="2,4,2,4"/> </Style> </Window.Resources> <Grid> <DockPanel UseLayoutRounding="True"> <StatusBar DockPanel.Dock="Bottom"> <StatusBarItem x:Name="MyStatusItem" Content="time"/> </StatusBar> <Menu DockPanel.Dock="Top" HorizontalAlignment="Right"> <StackPanel Orientation="Horizontal"> <Button x:Name="MyButtonItimatu模様" Content="市松模様" Click="MyButtonItimatu模様_Click"/> <Button x:Name="MyButtonToOrigin" Content="戻す" Click="MyButtonToOrigin_Click"/> <TextBlock Text="{Binding ElementName=MySliderViewScale, Path=Value, StringFormat=表示倍率\=x0}" VerticalAlignment="Center"/> <Slider x:Name="MySliderViewScale" Width="100" Minimum="1" Maximum="4" Focusable="False" Value="1" SmallChange="1" LargeChange="1" TickFrequency="1" IsSnapToTickEnabled="True" MouseWheel="MySlider_MouseWheel" VerticalAlignment="Center"/> <Button x:Name="MyButtonCopy" Content="コピー" Click="MyButtonCopy_Click"/> <Button x:Name="MyButtonPaste" Content="貼り付け" Click="MyButtonPaste_Click"/> <Button x:Name="MyButtonPasteBmp" Content="Bmp優先貼り" Click="MyButtonPasteBmp_Click"/> </StackPanel> </Menu> <StackPanel DockPanel.Dock="Right" Background="White"> <StackPanel Orientation="Horizontal"> <StackPanel> <Slider x:Name="MySliderScale" Minimum="1" Maximum="10" Value="2" Width="80" HorizontalAlignment="Center" TickFrequency="1" IsSnapToTickEnabled="True" MouseWheel="MySlider_MouseWheel" SmallChange="1" LargeChange="1"> <Slider.LayoutTransform> <RotateTransform Angle="270"/> </Slider.LayoutTransform> </Slider> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center"> <TextBlock Text="倍率="/> <TextBlock Text="{Binding ElementName=MySliderScale, Path=Value}"/> </StackPanel> </StackPanel> <StackPanel Margin="8,0,4,0"> <Slider x:Name="MySlider" Minimum="2" Maximum="10" SmallChange="1" TickFrequency="1" IsSnapToTickEnabled="True" Value="3" Width="80" HorizontalAlignment="Center" MouseWheel="MySlider_MouseWheel"> <Slider.LayoutTransform> <RotateTransform Angle="270"/> </Slider.LayoutTransform> </Slider> <TextBlock Text="{Binding ElementName=MySlider, Path=Value, StringFormat=n\=0}" HorizontalAlignment="Center"/> </StackPanel> </StackPanel> <Button x:Name="MyButton1" Content="縮小セパ" Click="MyButton1_Click"/> <Button x:Name="MyButton2" Content="拡大セパ" Click="MyButton2_Click"/> <Button x:Name="MyButton3" Content="縮小" Click="MyButton3_Click"/> <Button x:Name="MyButton4" Content="拡大" Click="MyButton4_Click"/> <!--<Button x:Name="MyButton5" Content="縮小TypeD" Click="MyButton5_Click"/>--> </StackPanel> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <Image x:Name="MyImage" Stretch="None"> <Image.LayoutTransform> <ScaleTransform ScaleX="{Binding ElementName=MySliderViewScale, Path=Value}" ScaleY="{Binding ElementName=MySliderViewScale, Path=Value}"/> </Image.LayoutTransform> </Image> </ScrollViewer> </DockPanel> </Grid> </Window>
MainWindow.xaml.cs
using System; using System.Linq; using System.Threading.Tasks; using System.Windows; using System.Windows.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Diagnostics;//処理時間計測用 //前回の画像縮小処理ではランチョスの使い方を間違っていたので書き直した - 午後わてんのブログ //https://gogowaten.hatenablog.com/entry/2021/05/06/144640 namespace _20210505_Lanczosで縮小 { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private BitmapSource MyBitmapOrigin; private BitmapSource MyBitmapOrigin32bit; private ImageBrush MyImageBrush; public MainWindow() { InitializeComponent(); #if DEBUG this.Top = 0; this.Left = 0; #endif MyImageBrush = MakeTileBrush(MakeCheckeredPattern(16, Colors.WhiteSmoke, Colors.LightGray)); //this.Background = MakeTileBrush(MakeCheckeredPattern(16, Colors.WhiteSmoke, Colors.LightGray)); RenderOptions.SetBitmapScalingMode(MyImage, BitmapScalingMode.NearestNeighbor); } //処理時間計測 private void MyExe( Func<BitmapSource, int, int, int, BitmapSource> func, BitmapSource source, int width, int height, int n) { var sw = new Stopwatch(); sw.Start(); var bitmap = func(source, width, height, n); sw.Stop(); MyStatusItem.Content = $"処理時間:{sw.Elapsed.TotalSeconds:000.000}秒, {func.Method.Name}"; MyImage.Source = bitmap; } //窓関数 private double Sinc(double d) { return Math.Sin(Math.PI * d) / (Math.PI * d); } /// <summary> /// ランチョス補完法での重み計算、拡大専用 /// </summary> /// <param name="d">距離</param> /// <param name="n">最大参照距離</param> /// <returns></returns> //private double GetLanczosWeight(double d, int n) //{ // if (d == 0) return 1.0; // else if (d > n) return 0.0; // else return Sinc(d) * Sinc(d / n); //} /// <summary> /// 縮小拡大両対応重み計算、Lanczos /// </summary> /// <param name="d">距離</param> /// <param name="n">最大参照距離</param> /// <param name="scale">倍率</param> /// <returns></returns> private double GetLanczosWeight(double d, int n, double limitD) { if (d == 0) return 1.0; else if (d > limitD) return 0.0; else return Sinc(d) * Sinc(d / n); } //セパラブルといっても全く同一じゃない、少し誤差が出るみたい //正確さを求めるなら使わないほうがいい /// <summary> /// 画像の縮小、ランチョス法で補完、PixelFormats.Bgra32専用) /// 通常版をセパラブルとParallelで高速化 /// </summary> /// <param name="source">PixelFormats.Bgra32のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgra32Ex(BitmapSource source, int width, int height, int n) { //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]; //倍率 double scale = width / (double)sourceWidth; //最大参照距離 = 逆倍率 * n double limitD = widthScale * n; //ピクセルでの最大参照距離、は指定距離*逆倍率の切り上げにした int actD = (int)Math.Ceiling(limitD); //拡大時の調整(これがないと縮小専用) if (1.0 < scale) { scale = 1.0;//重み計算に使うようで、拡大時は1固定 actD = n;//拡大時の実際の参照距離は指定距離と同じ } //横処理 _ = 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[] ws = GetFixWeihgts(rx, n, actD, scale, limitD); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; int pp; for (int xx = -actD; xx < actD; xx++) { int xc = xKijun + xx; //マイナス座標や画像サイズを超えていたら、収まるように修正 xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc; pp = (y * sourceStride) + (xc * pByte); double weight = ws[xx + actD]; //完全透明ピクセル(a=0)だった場合はRGBは計算しないで //重みだけ足し算して後で使う if (sourcePixels[pp + 3] == 0) { alphaFix += weight; continue; } bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; aSum += 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値の修正 //参照範囲がすべて完全透明だった場合は0のままでいいので計算しない if (alphaFix == 1) continue; //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する double rgbFix = 1 / (1 - alphaFix); bSum *= rgbFix; gSum *= rgbFix; rSum *= rgbFix; pp = y * stride + x * pByte; xResult[pp] = bSum; xResult[pp + 1] = gSum; xResult[pp + 2] = rSum; xResult[pp + 3] = aSum; } }); //縦処理 _ = 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[] ws = GetFixWeihgts(ry, n, actD, scale, limitD); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; int pp; for (int yy = -actD; yy < actD; yy++) { int yc = yKijun + yy; yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; pp = (yc * stride) + (x * pByte); double weight = ws[yy + actD]; if (xResult[pp + 3] == 0) { alphaFix += weight; continue; } bSum += xResult[pp] * weight; gSum += xResult[pp + 1] * weight; rSum += xResult[pp + 2] * weight; aSum += xResult[pp + 3] * weight; } if (alphaFix == 1) continue; //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する double rgbFix = 1 / (1 - alphaFix); bSum *= rgbFix; gSum *= rgbFix; rSum *= rgbFix; //0~255の範囲を超えることがあるので、修正 bSum = bSum < 0 ? 0 : bSum > 255 ? 255 : bSum; gSum = gSum < 0 ? 0 : gSum > 255 ? 255 : gSum; rSum = rSum < 0 ? 0 : rSum > 255 ? 255 : rSum; aSum = aSum < 0 ? 0 : aSum > 255 ? 255 : aSum; int ap = (y * stride) + (x * pByte); pixels[ap] = (byte)(bSum + 0.5); pixels[ap + 1] = (byte)(gSum + 0.5); pixels[ap + 2] = (byte)(rSum + 0.5); pixels[ap + 3] = (byte)(aSum + 0.5); } }); BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[] GetFixWeihgts(double r, int n, int actD, double scale, double limitD) { int nn = actD * 2;//全体の参照距離 //基準距離 double s = r - (int)r; double d = (s < 0.5) ? 0.5 - s : 0.5 - s + 1; //各重みと重み合計 double[] ws = new double[nn]; double sum = 0; for (int i = -actD; i < actD; i++) { double w = GetLanczosWeight(Math.Abs(d + i) * scale, n, limitD); sum += w; ws[i + actD] = w; } //重み合計で割り算して修正、全体で100%(1.0)にする for (int i = 0; i < nn; i++) { ws[i] /= sum; } return ws; } } /// <summary> /// 画像のリサイズ、ランチョス法で補完、PixelFormats.Bgra32専用) /// 高速化なし /// </summary> /// <param name="source">PixelFormats.Bgra32のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgra32(BitmapSource source, int width, int height, int n) { //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 scale = width / (double)sourceWidth; //最大参照距離 = 逆倍率 * n double limitD = widthScale * n; //実際の参照距離、は指定距離*逆倍率の切り上げにしたけど、切り捨てでも見た目の変化なし int actD = (int)Math.Ceiling(limitD); //int actD = (int)(limitD); //拡大時の調整(これがないと縮小専用) if (1.0 < scale) { scale = 1.0;//重み計算に使うようで、拡大時は1固定 actD = n;//拡大時の実際の参照距離は指定距離と同じ } 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[,] ws = GetFixWeights(rx, ry, actN, scale*0.1, n); double[,] ws = GetFixWeights(rx, ry, actD, scale, n, limitD); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; //参照範囲は基準から上(xは左)へnn、下(xは右)へnn-1の範囲 for (int yy = -actD; yy < actD; yy++) { int yc = yKijun + yy; //マイナス座標や画像サイズを超えていたら、収まるように修正 yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; for (int xx = -actD; xx < actD; xx++) { int xc = xKijun + xx; xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc; int pp = (yc * sourceStride) + (xc * pByte); double weight = ws[xx + actD, yy + actD]; //完全透明ピクセル(a=0)だった場合はRGBは計算しないで //重みだけ足し算して後で使う if (sourcePixels[pp + 3] == 0) { alphaFix += weight; continue; } bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; aSum += 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値の修正 //参照範囲がすべて完全透明だった場合は0のままでいいので計算しない if (alphaFix == 1) continue; //完全透明ピクセルが混じっていた場合は、その分を差し引いてRGB修正する double rgbFix = 1 / (1 - alphaFix); bSum *= rgbFix; gSum *= rgbFix; rSum *= rgbFix; //0~255の範囲を超えることがあるので、修正 bSum = bSum < 0 ? 0 : bSum > 255 ? 255 : bSum; gSum = gSum < 0 ? 0 : gSum > 255 ? 255 : gSum; rSum = rSum < 0 ? 0 : rSum > 255 ? 255 : rSum; aSum = aSum < 0 ? 0 : aSum > 255 ? 255 : aSum; int ap = (y * stride) + (x * pByte); pixels[ap] = (byte)(bSum + 0.5); pixels[ap + 1] = (byte)(gSum + 0.5); pixels[ap + 2] = (byte)(rSum + 0.5); pixels[ap + 3] = (byte)(aSum + 0.5); } } //_ = Parallel.For(0, height, y => // { // }); BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[,] GetFixWeights(double rx, double ry, int actN, double scale, int n, double limitD) { //全体の参照距離 int nn = actN * 2; //基準になる距離計算 double sx = rx - (int)rx; double sy = ry - (int)ry; double dx = (sx < 0.5) ? 0.5 - sx : 0.5 - sx + 1; double dy = (sy < 0.5) ? 0.5 - sy : 0.5 - sy + 1; //各ピクセルの重みと、重み合計を計算 double[] xw = new double[nn]; double[] yw = new double[nn]; double xSum = 0, ySum = 0; for (int i = -actN; i < actN; i++) { //距離に倍率を掛け算したのをLanczosで重み計算 double x = GetLanczosWeight(Math.Abs(dx + i) * scale, n, limitD); xSum += x; xw[i + actN] = x; double y = GetLanczosWeight(Math.Abs(dy + i) * scale, n, limitD); ySum += y; yw[i + actN] = y; } //重み合計で割り算して修正、全体で100%(1.0)にする for (int i = 0; i < nn; i++) { xw[i] /= xSum; yw[i] /= ySum; } // x * y double[,] ws = new double[nn, nn]; for (int y = 0; y < nn; y++) { for (int x = 0; x < nn; x++) { ws[x, y] = xw[x] * yw[y]; } } return ws; } } /// <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()) { //そのままだとAlphaがおかしいのでBgr32にコンバート source = new FormatConvertedBitmap(Clipboard.GetImage(), PixelFormats.Bgr32, null, 0); } return source; } /// <summary> /// クリップボードからBitmapSourceを取り出して返す、bmp優先、次にpng(アルファ値保持)形式に対応 /// </summary> /// <returns></returns> private BitmapSource GetImageFromClipboardBmpNextPNG() { BitmapSource source = null; BitmapSource img = Clipboard.GetImage(); //BMP if (img != null) { //そのままだとAlphaがおかしいのでBgr32にコンバート source = new FormatConvertedBitmap(img, PixelFormats.Bgr32, null, 0); } //PNG else { //クリップボードにPNG形式のデータがあったら、それを使ってBitmapFrame作成 using var ms = (System.IO.MemoryStream)Clipboard.GetData("PNG"); if (ms != null) { source = BitmapFrame.Create(ms, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); } } return source; } #region 市松模様作成 /// <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; } #endregion 市松模様作成 //ファイルドロップ時 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]); MyImage.Source = MyBitmapOrigin; MyBitmapOrigin32bit = MakeBitmapSourceBgra32FromFile(paths[0]); MyImage.Source = MyBitmapOrigin32bit; } //ボタンクリック private void MyButton1_Click(object sender, RoutedEventArgs e) { if (MyBitmapOrigin == null) return; int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / MySliderScale.Value); int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / MySliderScale.Value); MyExe(LanczosBgra32Ex, MyBitmapOrigin32bit, yoko, tate, (int)MySlider.Value); } private void MyButton2_Click(object sender, RoutedEventArgs e) { if (MyBitmapOrigin == null) return; int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value); int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value); MyExe(LanczosBgra32Ex, MyBitmapOrigin32bit, yoko, tate, (int)MySlider.Value); } private void MyButton3_Click(object sender, RoutedEventArgs e) { if (MyBitmapOrigin == null) return; int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / MySliderScale.Value); int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / MySliderScale.Value); MyExe(LanczosBgra32, MyBitmapOrigin32bit, yoko, tate, (int)MySlider.Value); } private void MyButton4_Click(object sender, RoutedEventArgs e) { if (MyBitmapOrigin == null) return; int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth * MySliderScale.Value); int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight * MySliderScale.Value); MyExe(LanczosBgra32, MyBitmapOrigin32bit, yoko, tate, (int)MySlider.Value); } //画像をクリップボードにコピー private void MyButtonCopy_Click(object sender, RoutedEventArgs e) { if (MyBitmapOrigin == null) return; ClipboardSetImageWithPng((BitmapSource)MyImage.Source); } //クリップボードから画像追加 private void MyButtonPaste_Click(object sender, RoutedEventArgs e) { BitmapSource img = GetImageFromClipboardWithPNG(); if (img != null) { //FormatConvertedBitmap bitmap = new(img, PixelFormats.Gray8, null, 0); //MyBitmapOrigin = bitmap; //MyImage.Source = bitmap; FormatConvertedBitmap bitmap = new(img, PixelFormats.Bgr24, null, 0); MyBitmapOrigin = bitmap; FormatConvertedBitmap bitmap32 = new(img, PixelFormats.Bgra32, null, 0); MyImage.Source = bitmap32; MyBitmapOrigin32bit = bitmap32; } } private void MySlider_MouseWheel(object sender, MouseWheelEventArgs e) { Slider slider = sender as Slider; if (e.Delta > 0) slider.Value += slider.SmallChange; else slider.Value -= slider.SmallChange; } private void MyButtonToOrigin_Click(object sender, RoutedEventArgs e) { //MyImage.Source = MyBitmapOrigin; MyImage.Source = MyBitmapOrigin32bit; } private void MyButton5_Click(object sender, RoutedEventArgs e) { if (MyBitmapOrigin == null) return; int yoko = (int)Math.Ceiling(MyBitmapOrigin.PixelWidth / MySliderScale.Value); int tate = (int)Math.Ceiling(MyBitmapOrigin.PixelHeight / MySliderScale.Value); } private void MyButtonItimatu模様_Click(object sender, RoutedEventArgs e) { if (this.Background == MyImageBrush) { this.Background = Brushes.White; } else { this.Background = MyImageBrush; } } private void MyButtonPasteBmp_Click(object sender, RoutedEventArgs e) { BitmapSource img = GetImageFromClipboardBmpNextPNG(); if (img != null) { FormatConvertedBitmap bitmap = new(img, PixelFormats.Bgr24, null, 0); MyBitmapOrigin = bitmap; FormatConvertedBitmap bitmap32 = new(img, PixelFormats.Bgra32, null, 0); MyImage.Source = bitmap32; MyBitmapOrigin32bit = bitmap32; } } #endregion コピペ } }
テスト
前回より2倍くらいかかるようになった、これは実際に参照する距離が延びたからだねえ
画質
参照距離nを延ばすと処理時間も延びるけど画質はシャープになる
文字画像
これを1/4倍で比較
今回のだとnはどれもそれぞれいいと思う、前回のはひどいw
図形画像
この画像はこちらから引用
Image Resizing for the Web and Email
https://www.cambridgeincolour.com/tutorials/image-resize-for-web.htm
これを1/2
これだとn=2がいいねえ
斜めの線
n=2は途切れている、n=8がいい気がするけど、n=4でもいいかなあ
輪郭
目立たないけど、n=8は文字やグラフ線の周りに輪郭が出ている
4倍に拡大してみると
nを高くすると変な輪郭が出る、写真画像とかだと気づかないけど、図形画像とかで輝度差が大きいエッジ部分に輪郭が出てしまうので、nは3か4あたりが処理時間も含めていいのかも
感想
できた!前回までだと拡大はいいんだけど、縮小の結果はいまいちだったからねえ、どこかが間違っているはずだけどそれがわからなくて、どうすれば斜めの線がガタガタにならなくなるのかを考えて、わからなくていろいろ試して、ようやくできた。終わってみれば単純なことで距離に倍率を掛け算したのを関数に渡すだけだった、これであっているのかはわからんけど、満足の行くきれいな縮小処理ができた
最初はランチョスでも全然きれいに縮小できないじゃんとがっかりしていたけど、使い方が間違っていたねえ、でも拡大と縮小で計算の仕方というか関数の使い方が違うなんてわかんないよ、縮小処理を理解できていたら気づくんだろうけどねえ
あとはこの調子だとバイリニアとバイキュービックの縮小も間違った方法で使っているはずだから、そっちもなんとかしたい
2021/05/08 11:59追記
重み計算のところが間違っていたのでコードを差し換えた
private double GetLanczosWeight(double d, int n, double scale) { if (d == 0) return 1.0; else if (d > n) return 0.0; else return Sinc(d * scale) * Sinc(d * scale / n); }
↑を↓にした
private double GetLanczosWeight(double d, int n, double limitD) { if (d == 0) return 1.0; else if (d > limitD) return 0.0; else return Sinc(d) * Sinc(d / n); }
後はこれに関連するところも書き直した
実際の参照距離は倍率によって変えることにしていたんだけど、コードでは間違って指定参照距離のnで計算していたので、limitD(逆倍率*n)で計算するように直した
これによって画質は
誤差程度きれいになったけど、処理時間が5倍くらいになってしまったので、修正しないほうが良かったまである
追記ここまで
蛇足
今回の解決に至るまでかなり遠回りしてきた
関連記事
次回は明日
gogowaten.hatenablog.com
前回のWPF記事は1週間前
gogowaten.hatenablog.com