WPF、画像ファイルを開いてBitmapSourceで取得するときにdpi変換とPixelFormat変換
画像ファイルをロックしないで開いてBitmapSourceを取得する
ロックしないっていうのは、画像を開いているときでも、元の画像ファイルの移動や削除、名前の変更ができる状態のこと
方法はファイルをStreamで開いて、そのStreamからBitmapFrame.CreateでBitmapSource作成
dpi変換はBitmapSource.Create時に指定
PixelFormat変換はFormatConvertedBitmapクラスを使う
単純に取得(dpiやPixcelFormatの変更なし)
/// <summary> /// ファイルから画像読み込み /// </summary> /// <param name="filePath">画像ファイルパス</param> /// <returns></returns> private BitmapSource MakeBitmapSourceFromFile(string filePath) { BitmapSource source = null; try { using (var stream = System.IO.File.OpenRead(filePath)) { source = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); } } catch (Exception) { } return source; }
最低限ならusingからの4行でok
streamで取得部分の新旧
(以前) using (var stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read))
こうしていたのを
(今回) using (var stream = System.IO.File.OpenRead(filePath))
単に開くだけならこれでいいみたい、短くなった
StreamからBitmapSource作成部分
BitmapFrame.Create
(誤)source = BitmapFrame.Create(stream);
(正)source = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad);
BitmapCacheOption.OnLoadっていうオプションを指定しないと、return sourceのところでは揮発?していて画像取得できなかった
dpiやPixelFormatを指定(変更)して取得
dpiを指定して取得
/// <summary> /// dpiを指定してファイルから画像読み込み /// </summary> /// <param name="filePath"></param> /// <param name="dpiX"></param> /// <param name="dpiY"></param> /// <returns></returns> private BitmapSource MakeBitmapSourceDpiFromFile(string filePath, double dpiX = 96, double dpiY = 96) { BitmapSource source = null; try { using (var stream = System.IO.File.OpenRead(filePath)) { var frame = BitmapFrame.Create(stream); int w = frame.PixelWidth; int h = frame.PixelHeight; int stride = (w * frame.Format.BitsPerPixel + 7) / 8; var pixels = new byte[h * stride]; frame.CopyPixels(pixels, stride, 0); source = BitmapSource.Create(w, h, dpiX, dpiY, frame.Format, frame.Palette, pixels, stride); }; } catch (Exception) { } return source; }
dpiは直接変更(指定)できないので、BitmapSourceのCreateメソッドを使ってBitmapFrameから作り直すことになる
dpi引数の既定値は画像の表示がドットバイドットになるようにパソコンディスプレイ標準の96にしている、と言っても最近は24インチや27インチの4Kディスプレイでスケーリングして使っている人も増えてるから標準ってわけじゃないかもねえ
ファイルを開いてStreamで取得するとこまではさっきと同じ
BitmapFrame.CreateでのBitmapCacheOption.OnLoadは必要ない
作成したBitmapFrameからBitmapSourceのCreateメソッドでBitmapSource作成
PixelFormatを指定(変更)して取得
/// <summary> /// PixelFormatとdpiを指定してファイルから画像読み込み /// </summary> /// <param name="filePath">フルパス</param> /// <param name="format">ピクセルフォーマット、画像と違ったときは指定フォーマットにコンバートする</param> /// <param name="dpiX"></param> /// <param name="dpiY"></param> /// <returns></returns> private BitmapSource MakeBitmapSourceFormatFromFile(string filePath, PixelFormat format, 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 != format) { source = new FormatConvertedBitmap(source, format, null, 0); } int w = source.PixelWidth; int h = source.PixelHeight; int stride = (w * source.Format.BitsPerPixel + 7) / 8; var pixels = new byte[h * stride]; source.CopyPixels(pixels, stride, 0); source = BitmapSource.Create(w, h, dpiX, dpiY, format, source.Palette, pixels, stride); }; } catch (Exception) { } return source; }
PixelFormatもdpi同様指定すれば変更されるってわけじゃないので、FormatConvertedBitmapクラスを使って変換(コンバート)する。ここでは変換後にdpiを指定するためにBitmapSourceを作成しているけど、PixelFormatの指定だけなら必要ないかも
PixelFormatをBgar32に変換して読み込み
/// <summary> /// PixelFormatをBgar32固定、dpiは指定してファイルから画像読み込み /// </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; var pixels = new byte[h * stride]; source.CopyPixels(pixels, stride, 0); source = BitmapSource.Create(w, h, dpiX, dpiY, PixelFormats.Bgra32, source.Palette, pixels, stride); }; } catch (Exception) { } return source; }
どんな画像もBgar32、dpi96に変換して取得
画像処理するときのPixelFormatはカラーならBgar32、グレースケールならGray8が使いやすいと思う
カラーならPbgra32のほうが処理が速いらしいんだけど
gogowaten.hatenablog.com
こういうこともあるからBgar32ほうがいいかなあと
テストアプリのコード
作成動作環境
- Windows 10 Home バージョン 2004
- Visual Studio Community 2019
- WPF
- C#
- .NET 5
MainWindow.xaml
<Window x:Class="_20210321_画像読み込み.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:_20210321_画像読み込み" mc:Ignorable="d" Title="MainWindow" Height="560" Width="800" Drop="Window_Drop" AllowDrop="True"> <Grid UseLayoutRounding="True"> <DockPanel> <StatusBar DockPanel.Dock="Bottom"> <StatusBarItem Name="MyStatus" Content="status"/> </StatusBar> <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"> <Image x:Name="MyImage" StretchDirection="DownOnly"/> </ScrollViewer> </DockPanel> </Grid> </Window>
MainWindow.xaml.cs
using System; using System.Linq; using System.Windows; using System.Windows.Media; using System.Windows.Media.Imaging; namespace _20210321_画像読み込み { /// <summary> /// Interaction logic for MainWindow.xaml /// </summary> public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); //拡大縮小表示の補完方法指定(dpiが96以外のとき用)、指定なしだとバイリニア法 //再近傍補完法 //MyImage.Loaded += (s, e) => { RenderOptions.SetBitmapScalingMode(MyImage, BitmapScalingMode.NearestNeighbor); }; //高画質(バイキュービック法?) //MyImage.Loaded += (s, e) => { RenderOptions.SetBitmapScalingMode(MyImage, BitmapScalingMode.Fant); }; } //ドロップされたファイル群の先頭ファイルから画像読み込み 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(); //BitmapSource source = MakeBitmapSourceFromFile(paths[0]); //BitmapSource source = MakeBitmapSourceDpiFromFile(paths[0]); //BitmapSource source = MakeBitmapSourceDpiFromFile(paths[0], 120, 120); //BitmapSource source = MakeBitmapSourceFormatFromFile(paths[0], PixelFormats.Indexed2); //BitmapSource source = MakeBitmapSourceFormatFromFile(paths[0], PixelFormats.Indexed2, 192, 192); //BitmapSource source = MakeBitmapSourceFormatFromFile(paths[0], PixelFormats.Indexed2, 48, 48); //BitmapSource source = MakeBitmapSourceFormatFromFile(paths[0], PixelFormats.Gray8); BitmapSource source = MakeBitmapSourceBgra32FromFile(paths[0]); source.Freeze();//要る? MyImage.Source = source; MyStatus.Content = $"dpi = {source.DpiX}, PixelFormat = {source.Format}"; } /// <summary> /// PixelFormatsやdpiなどは元の画像のまま読み込み /// </summary> /// <param name="filePath">フルパス</param> /// <returns></returns> private BitmapSource MakeBitmapSourceFromFile(string filePath) { BitmapSource source = null; try { //using (var stream = System.IO.File.OpenRead(filePath)) //{ // source = BitmapFrame.Create(stream); //} //↑この時点では画像取得できているけど、returnのところでは真っ白になる //↓はBitmapCacheOption.OnLoadを指定、これなら期待通りに取得できる using (var stream = System.IO.File.OpenRead(filePath)) { source = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); } //以前の方法、FileStreamクラスを使っていた //using (var stream = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read)) //{ // source = BitmapFrame.Create(stream, BitmapCreateOptions.None, BitmapCacheOption.OnLoad); //}; } catch (Exception) { } return source; } // /// <summary> /// dpiを指定してファイルから画像読み込み /// </summary> /// <param name="filePath"></param> /// <param name="dpiX"></param> /// <param name="dpiY"></param> /// <returns></returns> private BitmapSource MakeBitmapSourceFromFile(string filePath, double dpiX = 96, double dpiY = 96) { BitmapSource source = null; try { using (var stream = System.IO.File.OpenRead(filePath)) { var frame = BitmapFrame.Create(stream); int w = frame.PixelWidth; int h = frame.PixelHeight; int stride = (w * frame.Format.BitsPerPixel + 7) / 8; var pixels = new byte[h * stride]; frame.CopyPixels(pixels, stride, 0); source = BitmapSource.Create(w, h, dpiX, dpiY, frame.Format, frame.Palette, pixels, stride); }; } catch (Exception) { } return source; } /// <summary> /// PixelFormatとdpiを指定してファイルから画像読み込み /// </summary> /// <param name="filePath">フルパス</param> /// <param name="format">ピクセルフォーマット、画像と違ったときは指定フォーマットにコンバートする</param> /// <param name="dpiX"></param> /// <param name="dpiY"></param> /// <returns></returns> private BitmapSource MakeBitmapSourceFromFile(string filePath, PixelFormat format, 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 != format) { source = new FormatConvertedBitmap(source, format, null, 0); } int w = source.PixelWidth; int h = source.PixelHeight; int stride = (w * source.Format.BitsPerPixel + 7) / 8; var pixels = new byte[h * stride]; source.CopyPixels(pixels, stride, 0); source = BitmapSource.Create(w, h, dpiX, dpiY, format, source.Palette, pixels, stride); }; } catch (Exception) { } return source; } // /// <summary> /// PixelFormatをBgar32固定、dpiは指定してファイルから画像読み込み /// </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; var pixels = new byte[h * stride]; source.CopyPixels(pixels, stride, 0); source = BitmapSource.Create(w, h, dpiX, dpiY, PixelFormats.Bgra32, source.Palette, pixels, stride); }; } catch (Exception) { } return source; } } }
35~42行目あたりのコメントアウトを外して使う
ウィンドウに画像ファイルドロップで開く
テストに使う画像は
503LVっていうスマホで撮影した普通のjpeg画像ファイル、サイズは653x490、dpi72
dpi72、PixelFormatはBgr32で取得された
dpiが標準の96より小さい72なので、元の画像より引き伸ばされた結果、ぼやけて表示されている
横、653*96/72=870.66667
縦、490*96/72=653.33333
dpi96だとムダな引き伸ばしがされないので、くっきり表示された
Indexed2は表示色数が2bit(4色)のPixelFormat
いいね
元の画像が普通のjpegなのでさっきのBgr32と全く同じ見た目だけどねえ
半透明を扱う画像処理ならBgra32
扱わないならRgb24もいいかも
参照したところ
qiita.com
今回の記事はここを見て、なるほどこんな方法もあるのかと思って書いた
関連記事
前回のWPF記事は16日前
3年前
gogowaten.hatenablog.com
このときは
bitmapFrame = BitmapFrame.Create( fs, BitmapCreateOptions.PreservePixelFormat, BitmapCacheOption.Default); }
ってBitmapFrame作成時にいろいろオプションを指定している
BitmapCreateOptions.PreservePixelFormat,
とくにこれ、元の画像のPixelFormatに合わせるオプションみたい
gogowaten.hatenablog.com
ほとんど憶えがない、こんなの作ってたんだなあ
gogowaten.hatenablog.com
これは憶えがある