午後わてんのブログ

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

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ほうがいいかなあと


テストアプリのコード

github.com




作成動作環境




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行目あたりのコメントアウトを外して使う


f:id:gogowaten:20210322135511p:plain
テストアプリ
ウィンドウに画像ファイルドロップで開く

テストに使う画像は

f:id:gogowaten:20210322135731j:plain
テスト画像
f:id:gogowaten:20210322135753p:plain
テスト画像の詳細
503LVっていうスマホで撮影した普通のjpeg画像ファイル、サイズは653x490、dpi72

f:id:gogowaten:20210322143739p:plain
PixelFormat、dpi指定なし
f:id:gogowaten:20210322141318p:plain
結果
dpi72、PixelFormatはBgr32で取得された
dpiが標準の96より小さい72なので、元の画像より引き伸ばされた結果、ぼやけて表示されている
横、653*96/72=870.66667
縦、490*96/72=653.33333


f:id:gogowaten:20210322143908p:plainf:id:gogowaten:20210322143912p:plain
dpiを96指定
dpi96だとムダな引き伸ばしがされないので、くっきり表示された

f:id:gogowaten:20210322144419p:plainf:id:gogowaten:20210322144421p:plain
PixelsFormatにIndexed2を指定、dpiは96を指定
Indexed2は表示色数が2bit(4色)のPixelFormat

f:id:gogowaten:20210322144842p:plainf:id:gogowaten:20210322144845p:plain
PixelFormatにBgra32、dpiに96を指定
いいね
元の画像が普通のjpegなのでさっきのBgr32と全く同じ見た目だけどねえ
半透明を扱う画像処理ならBgra32
扱わないならRgb24もいいかも




参照したところ

qiita.com 今回の記事はここを見て、なるほどこんな方法もあるのかと思って書いた






関連記事
前回のWPF記事は16日前

gogowaten.hatenablog.com



3年前
gogowaten.hatenablog.com このときは

bitmapFrame = BitmapFrame.Create(
fs,
BitmapCreateOptions.PreservePixelFormat,
BitmapCacheOption.Default);
}

ってBitmapFrame作成時にいろいろオプションを指定している
BitmapCreateOptions.PreservePixelFormat,
とくにこれ、元の画像のPixelFormatに合わせるオプションみたい

gogowaten.hatenablog.com ほとんど憶えがない、こんなの作ってたんだなあ

gogowaten.hatenablog.com これは憶えがある

2年前
gogowaten.hatenablog.com

1年前
gogowaten.hatenablog.com