C#、WPF、ランチョス補完法での画像のリサイズ、24bit(普通のカラー)と32bit(半透明画像)対応版
昨日のグレースケール専用だったのをカラー対応にしただけ
コード
24bitカラー対応版
//窓関数 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> /// 画像の拡大縮小、ランチョス法で補完、PixelFormats.Bgr24専用) /// 通常版 /// </summary> /// <param name="source">PixelFormats.Bgr24のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgr24(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]; 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); //修正した重み取得 var ws = GetFixWeights(rx, ry, n); double bSum = 0, gSum = 0, rSum = 0; //参照範囲は基準から上(xは左)へn、下(xは右)へn-1の範囲 for (int yy = -n; yy < n; yy++) { int yc = yKijun + yy; //マイナス座標や画像サイズを超えていたら、収まるように修正 yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; for (int xx = -n; xx < n; 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 + n, yy + n]; bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; } } //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; 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); } }; BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[,] GetFixWeights(double rx, double ry, int n) { int nn = n * 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 = -n; i < n; i++) { double x = GetLanczosWeight(Math.Abs(dx + i), n); xSum += x; xw[i + n] = x; double y = GetLanczosWeight(Math.Abs(dy + i), n); ySum += y; yw[i + n] = 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; } }
32bitカラー対応版
//窓関数 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> /// 画像の拡大縮小、ランチョス法で補完、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]; 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); //修正した重み取得 var ws = GetFixWeights(rx, ry, n); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; //参照範囲は基準から上(xは左)へn、下(xは右)へn-1の範囲 for (int yy = -n; yy < n; yy++) { int yc = yKijun + yy; //マイナス座標や画像サイズを超えていたら、収まるように修正 yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; for (int xx = -n; xx < n; 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 + n, yy + n]; //完全透明ピクセル(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); } }; BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[,] GetFixWeights(double rx, double ry, int n) { int nn = n * 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 = -n; i < n; i++) { double x = GetLanczosWeight(Math.Abs(dx + i), n); xSum += x; xw[i + n] = x; double y = GetLanczosWeight(Math.Abs(dy + i), n); ySum += y; yw[i + n] = 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; } }
テストアプリ
- 画像ファイルドロップかクリップボードからの貼り付けで画像表示
- 倍率は1~10
- 倍率=2で縮小は1/2、倍率=5で縮小は1/5
- 32bitボタンは半透明画像対応
- n=3が標準、大きくすると処理時間かかる
- 左下に処理時間表示
- 処理中は操作不能
- 空きメモリが2GB以下と少ないときに、大きな画像(1000x1000以上)を10倍拡大処理するとアプリが落ちる
作成動作環境
- Windows 10 Home バージョン 2004
- Visual Studio Community 2019
- WPF
- C#
- .NET 5
- CPU AMD Ryzen 5 2400G(4コア8スレッド)
- MEM DDR4-2666
.NET 5がインストールされているWindows 10が必要、.NET Frameworkでは動かないはず
ダウンロード
ここの20210428_Lanczos.32bit.zip
github.com
コード
MainWindow.xaml
<Window x:Class="_20210428_Lanczosで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:_20210428_Lanczosで32bit画像リサイズ" mc:Ignorable="d" Title="MainWindow" Height="487" Width="634" AllowDrop="True" Drop="Window_Drop"> <Window.Resources> <Style TargetType="Button"> <Setter Property="Margin" Value="2,10,2,0"/> </Style> </Window.Resources> <Grid> <DockPanel UseLayoutRounding="True"> <StatusBar DockPanel.Dock="Bottom"> <StatusBarItem x:Name="MyStatusItem" Content="time"/> </StatusBar> <StackPanel DockPanel.Dock="Right" Background="White"> <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> <Button x:Name="MyButton1" Content="縮小(24bit)" Click="MyButton1_Click"/> <Button x:Name="MyButton2" Content="拡大(24bit)" Click="MyButton2_Click"/> <Button x:Name="MyButton3" Content="縮小(32bit)" Click="MyButton3_Click"/> <Button x:Name="MyButton4" Content="拡大(32bit)" Click="MyButton4_Click"/> <Button x:Name="MyButtonToOrigin" Content="戻す" Click="MyButtonToOrigin_Click"/> <Slider x:Name="MySlider" Minimum="2" Maximum="6" SmallChange="1" TickFrequency="1" IsSnapToTickEnabled="True" Value="3" Width="100" 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"/> <Button x:Name="MyButtonCopy" Content="コピ" Click="MyButtonCopy_Click"/> <Button x:Name="MyButtonPaste" Content="ペ" Click="MyButtonPaste_Click"/> </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.Controls; using System.Windows.Input; using System.Windows.Media; using System.Windows.Media.Imaging; using System.Diagnostics; namespace _20210428_Lanczosで32bit画像リサイズ { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { private BitmapSource MyBitmapOrigin; private BitmapSource MyBitmapOrigin32bit; public MainWindow() { InitializeComponent(); #if DEBUG this.Top = 0; this.Left = 0; #endif this.Background = MakeTileBrush(MakeCheckeredPattern(16, Colors.WhiteSmoke, Colors.LightGray)); } //処理時間計測 private void MyExe(Func<BitmapSource, int, int, int, BitmapSource> func, BitmapSource source, int width, int height, int a) { var sw = new Stopwatch(); sw.Start(); var bitmap = func(source, width, height, a); 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); } ////ランチョス改変、ボカシが入る //private double GetLanczosWeightA(double d, int n) //{ // if (d == 0) return 1.0; // else if (d > n) return 0.0; // else return Sinc(d / n); //} ////ランチョス改変、ボカシが入る //private double GetLanczosWeightB(double d, int n) //{ // if (d == 0) return 1.0; // else if (d > n) return 0.0; // else return Sinc(d / n * 1.4);//n=1.0~1.5程度、1.0ソフト~1.5シャープ //} /// <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]; //横処理 _ = 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); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; int pp; for (int xx = -n; xx < n; xx++) { int xc = xKijun + xx; //マイナス座標や画像サイズを超えていたら、収まるように修正 xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc; pp = (y * sourceStride) + (xc * pByte); double weight = ws[xx + n]; //完全透明ピクセル(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); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; int pp; for (int yy = -n; yy < n; yy++) { int yc = yKijun + yy; yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; pp = (yc * stride) + (x * pByte); double weight = ws[yy + n]; 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 nn = n * 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 = -n; i < n; i++) { double w = GetLanczosWeight(Math.Abs(d + i), n); sum += w; ws[i + n] = w; } //重み合計で割り算して修正、全体で100%(1.0)にする for (int i = 0; i < nn; i++) { ws[i] /= sum; } return ws; } } /// <summary> /// 画像の拡大縮小、ランチョス法で補完、PixelFormats.Bgr24専用) /// 通常版をセパラブルとParallelで高速化 /// </summary> /// <param name="source">PixelFormats.Bgr24のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgr24Ex(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]; //横処理 _ = 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); double bSum = 0, gSum = 0, rSum = 0; int pp; for (int xx = -n; xx < n; xx++) { int xc = xKijun + xx; //マイナス座標や画像サイズを超えていたら、収まるように修正 xc = xc < 0 ? 0 : xc > sourceWidth - 1 ? sourceWidth - 1 : xc; pp = (y * sourceStride) + (xc * pByte); double weight = ws[xx + n]; bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; } pp = y * stride + x * pByte; xResult[pp] = bSum; xResult[pp + 1] = gSum; xResult[pp + 2] = rSum; } }); //縦処理 _ = 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); double bSum = 0, gSum = 0, rSum = 0; int pp; for (int yy = -n; yy < n; yy++) { int yc = yKijun + yy; yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; pp = (yc * stride) + (x * pByte); double weight = ws[yy + n]; bSum += xResult[pp] * weight; gSum += xResult[pp + 1] * weight; rSum += xResult[pp + 2] * weight; } //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; 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); } }); BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[] GetFixWeihgts(double r, int n) { int nn = n * 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 = -n; i < n; i++) { double w = GetLanczosWeight(Math.Abs(d + i), n); sum += w; ws[i + n] = 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]; 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); //修正した重み取得 var ws = GetFixWeights(rx, ry, n); double bSum = 0, gSum = 0, rSum = 0, aSum = 0; double alphaFix = 0; //参照範囲は基準から上(xは左)へn、下(xは右)へn-1の範囲 for (int yy = -n; yy < n; yy++) { int yc = yKijun + yy; //マイナス座標や画像サイズを超えていたら、収まるように修正 yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; for (int xx = -n; xx < n; 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 + n, yy + n]; //完全透明ピクセル(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); } }; BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[,] GetFixWeights(double rx, double ry, int n) { int nn = n * 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 = -n; i < n; i++) { double x = GetLanczosWeight(Math.Abs(dx + i), n); xSum += x; xw[i + n] = x; double y = GetLanczosWeight(Math.Abs(dy + i), n); ySum += y; yw[i + n] = 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.Bgr24専用) /// 通常版 /// </summary> /// <param name="source">PixelFormats.Bgr24のBitmap</param> /// <param name="width">変換後の横ピクセル数を指定</param> /// <param name="height">変換後の縦ピクセル数を指定</param> /// <param name="n">最大参照距離、3か4がいい</param> /// <returns></returns> private BitmapSource LanczosBgr24(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]; 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); //修正した重み取得 var ws = GetFixWeights(rx, ry, n); double bSum = 0, gSum = 0, rSum = 0; //参照範囲は基準から上(xは左)へn、下(xは右)へn-1の範囲 for (int yy = -n; yy < n; yy++) { int yc = yKijun + yy; //マイナス座標や画像サイズを超えていたら、収まるように修正 yc = yc < 0 ? 0 : yc > sourceHeight - 1 ? sourceHeight - 1 : yc; for (int xx = -n; xx < n; 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 + n, yy + n]; bSum += sourcePixels[pp] * weight; gSum += sourcePixels[pp + 1] * weight; rSum += sourcePixels[pp + 2] * weight; } } //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; 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); } }; BitmapSource bitmap = BitmapSource.Create(width, height, 96, 96, source.Format, null, pixels, stride); return bitmap; //修正した重み取得 double[,] GetFixWeights(double rx, double ry, int n) { int nn = n * 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 = -n; i < n; i++) { double x = GetLanczosWeight(Math.Abs(dx + i), n); xSum += x; xw[i + n] = x; double y = GetLanczosWeight(Math.Abs(dy + i), n); ySum += y; yw[i + n] = 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()) { 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 = 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(LanczosBgr24, MyBitmapOrigin, yoko, tate, (int)MySlider.Value); MyExe(LanczosBgr24Ex, MyBitmapOrigin, 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(LanczosBgr24Ex, MyBitmapOrigin, 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(LanczosBgra32Ex, 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(LanczosBgra32Ex, 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; } #endregion コピペ } }
テスト
いつもの画像
ごく普通の写真画像
1/2倍
1/3倍
3倍
いつもの32bit(半透明)画像
半透明なので背景の市松模様が透けて見える
1/3倍
できてる
拡大もできた!
他の補完法と比較
斜めがきれい、バイキュービックもきれいだけどランチョスのほうがきれいな気がする
nの値は3がいいかな、n=2だと斜めのガタガタが目立つ、n=4以上だと変な輪郭が出てくる
10倍拡大処理
ランチョスは変な輪郭が若干出ているけど、斜めや曲線部分はバイキュービックよりきれい
処理時間
1088x816の画像を10倍拡大で、10880x8160にしたときの処理時間
nが増えるに従ってリニアに処理時間も増えた、参照範囲は2乗で増えるからもっと増えるのかと思ったけど、縦横別計算で処理しているからリニアなのかも、縦横別計算のセパラブルすごいな!
バイキュービックと比較するとn=2でも4.574/0.77=5.9402597
約6倍も時間がかかっている、これで画質も6倍きれいならいいけど、誤差程度だから、このままだとランチョスの出番はないねえ
感想
できた!再挑戦編完結、32bit対応はバイキュービックのときの失敗があったから簡単にできた
3年前のと比べると縦横別計算での高速化ができたのと、明らかに間違っていたバイキュービックを直せたのが特に良かった、あとはピクセルの中心で計算するようにしたからより正確になったはず
縮小処理がきれいにならないのが不満
斜めの線や、斜めの輪郭が縮小処理で跡切れ跡切れになったり、ガタガタになるのが気になる、これはランチョスだけじゃなくバイキュービック、バイリニアでもこうなる
最初はバイリニア特有のものなのかと思っていたけど、そうじゃないみたい?
元はと言えば絶対画連合連画での縮小画像をきれいにしたくて、バイキュービックとランチョスに再挑戦を始めたんだけどねえ、そこは残念
関連記事
次回は1週間後
gogowaten.hatenablog.com
縮小処理が間違っていたねえ
前回のWPF記事は昨日
gogowaten.hatenablog.com
バイキュービックは2日前
gogowaten.hatenablog.com
バイリニアは11日前
gogowaten.hatenablog.com