午後わてんのブログ

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

WPF、画像から複数箇所を矩形(Rect)に切り抜いて、それぞれ位置を合わせて1枚の画像にしてファイルに保存する

表現が難しい、百聞は一見にしかず

f:id:gogowaten:20210124193141p:plain
元の画像と、切り抜き後の画像
こういうの

作っている環境


2021WPF/20210124_画像の切り抜き、複数画像を1枚にする
github.com

MainWindow.xaml

<Window x:Class="_20210124_画像の切り抜き_複数画像を1枚にする.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:_20210124_画像の切り抜き_複数画像を1枚にする"
        mc:Ignorable="d"
        Title="MainWindow" Height="650" Width="800">
  <Window.Resources>
    <Style TargetType="Image">
      <Setter Property="Margin" Value="10"/>
      <Setter Property="Stretch" Value="None"/>
    </Style>
  </Window.Resources>
  <Grid UseLayoutRounding="True">
    <StackPanel>
      <Image x:Name="MyOriginImage"/>
      <Image x:Name="MyImage"/>
    </StackPanel>
  </Grid>
</Window>


MainWindow.xaml.cs

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;

namespace _20210124_画像の切り抜き_複数画像を1枚にする
{
    public partial class MainWindow : Window
    {
        public MainWindow()
        {
            InitializeComponent();

            MyInitialize();
        }

        private void MyInitialize()
        {
            //元の画像の読み込みと表示
            var img = new BitmapImage(new Uri(
                @"D:\ブログ用\チェック用2\WP_20201222_10_21_40_Pro_2020_12_22_午後わてん_ラーメン.jpg"));
            MyOriginImage.Source = img;

            //切り抜き範囲のリスト作成
            List<Rect> MyRectList = new()
            {
                new Rect(5, 110, 85, 60),
                new Rect(65, 135, 130, 130),
                new Rect(270, 50, 135, 130),
            };

            //切り抜いて
            BitmapSource bmp = CroppedBitmapFromRects(img, MyRectList);
            MyImage.Source = bmp;//表示
            SaveImage(bmp);//保存
        }

        /// <summary>
        /// 複数Rect範囲を組み合わせた形にbitmapを切り抜く
        /// </summary>
        /// <param name="source">元の画像</param>
        /// <param name="rectList">Rectのコレクション</param>
        /// <returns></returns>
        private BitmapSource CroppedBitmapFromRects(BitmapSource source, List<Rect> rectList)
        {
            var dv = new DrawingVisual();

            using (DrawingContext dc = dv.RenderOpen())
            {
                //それぞれのRect範囲で切り抜いた画像を描画していく
                foreach (var rect in rectList)
                {
                    dc.DrawImage(new CroppedBitmap(source, RectToIntRectWith切り捨て(rect)), rect);
                }
            }

            //描画位置調整
            dv.Offset = new Vector(-dv.ContentBounds.X, -dv.ContentBounds.Y);

            //bitmap作成、縦横サイズは切り抜き後の画像全体がピッタリ収まるサイズにする
            //PixelFormatsはPbgra32で決め打ち、これ以外だとエラーになるかも、
            //画像を読み込んだbitmapImageのPixelFormats.Bgr32では、なぜかエラーになった
            var bmp = new RenderTargetBitmap(
                (int)Math.Ceiling(dv.ContentBounds.Width),
                (int)Math.Ceiling(dv.ContentBounds.Height),
                96, 96, PixelFormats.Pbgra32);

            bmp.Render(dv);
            return bmp;
        }

        //RectからInt32Rect作成、小数点以下切り捨て編
        private Int32Rect RectToIntRectWith切り捨て(Rect re)
        {
            return new Int32Rect((int)re.X, (int)re.Y, (int)re.Width, (int)re.Height);
        }

        //今回は未使用
        //RectからInt32Rect作成、小数点以下四捨五入
        private Int32Rect RectToIntRectWith簡易四捨五入(Rect re)
        {
            return new Int32Rect(
                My簡易四捨五入(re.X),
                My簡易四捨五入(re.Y),
                My簡易四捨五入(re.Width),
                My簡易四捨五入(re.Height));

            //double型からint型の変換は切り捨てなので、
            //0.5足してから変換すると簡易四捨五入になる
            int My簡易四捨五入(double value)
            {
                return (int)(value + 0.5);
            }
        }

        //bitmapをpng画像ファイルで保存
        private void SaveImage(BitmapSource source)
        {
            PngBitmapEncoder encoder = new();
            encoder.Frames.Add( BitmapFrame.Create(source));
            string path = DateTime.Now.ToString("HH時mm分ss秒");
            path = "cropped_" + path + ".png";
            using (var pp = new System.IO.FileStream(
                path, System.IO.FileMode.Create, System.IO.FileAccess.Write))
            {
                encoder.Save(pp);
            }
        }

    }
}


実行後のアプリの状態は

f:id:gogowaten:20210124224741p:plain
今回のアプリ
上に元の画像、下に切り抜き画像が表示される、これは確認用で、本命の切り抜き画像ファイルはアプリの実行ファイルのあるフォルダにできる

元の画像

f:id:gogowaten:20210124211111p:plain
ラーメンに豆腐は最高
給付金によって9年ぶりにラーメン食べる機会を得られて、驚くほど美味しかったのはマルちゃん正麺醤油味、なお写真は日清ラ王味噌 2食/5食パックx6、これも美味しい

切り抜く場所とサイズになる矩形範囲のRectは

f:id:gogowaten:20210124195320p:plain
切り抜き範囲のリスト作成
今回は決め打ちで、3箇所にした
これは図にしてみると
f:id:gogowaten:20210124215027p:plain
3Rectはどういう集まりなんだっけ?
こんな感じで、さらに画像と合わせてみると

f:id:gogowaten:20210124203424p:plain
もと画像と3つのRect
こう


CroppedBitmapクラス
画像の切り抜きはCroppedBitmapクラスを使うとラクにできる
CroppedBitmapに渡すのは元の画像とInt32Rect、画像のサイズに小数点はないからRectじゃなくてInt32Rectなので
RectをInt32Rectに変換

f:id:gogowaten:20210124200536p:plain
RectからInt32Rect
これは単純にdouble型をint型にキャストしているから小数点以下は切り捨てになる、今回の決め打ちRectは整数しか入っていのがわかっているからこれでいい
そうじゃないときは切り上げしたほうがいい、Mathクラスの切り上げのメソッドCeiling

切り抜いた画像群を位置合わせして1枚の画像にする

f:id:gogowaten:20210124200410p:plain
それぞれのRectに従って切り抜いて1枚の画像にする

DrawingVisualとDrawingContxtとRenderTargetBitmapクラスを使っている
と言ってもよくわかっていない
要なのは54行目かな
CroppedBitmapで得た切り抜き画像を、DrawingContxtのDrawImageメソッドでDrawしている
ってことはDrawingVisualが画用紙とかキャンバスみたいな感じなのかなあ、それともDrawingContxtが画用紙とかキャンバス?この辺のイメージがねえ、わからん
あと重要なのが、59行目のOffsetでの位置調整で、これをしないと

f:id:gogowaten:20210124210231p:plain
位置調整しなかった場合
赤の点線が画像の範囲になって、豆腐が欠ける、コップの右も地味に欠けている
Offsetする値は、DrawingVisualのContentBoundsプロパティのXとYをマイナスにした値でぴったりになった

最後にBitmapの作成はRenderTargeBitmapクラスを使う、RenderメソッドにDrawingVisualを渡して完成!
RenderTargeBitmapの作成時に渡しているのは、

f:id:gogowaten:20210124232441p:plain
RenderTargeBitmap作成
ピクセル数、縦ピクセル数、横dpi、縦dpi、ピクセルフォーマット

できあがりの画像の縦横サイズは切り抜き画像がピッタリ収まるサイズにしたい、そうしないでもとの画像サイズにすると余計な余白ができてしまう

f:id:gogowaten:20210124205118p:plain
画像の縦横サイズの調整
赤枠がもとの画像サイズ、緑枠がぴったりサイズ
で、このピッタリサイズは位置調整にも使ったDrawingVisualのContentBoundsプロパティ、これのWidthとHeight、それを利用しているのが65、66行目
dpiはWindows標準の96で決め打ちしているけど、元の画像に合わせたほうがいいかもしれない?今回は元画像のdpiも96だった
ピクセルフォーマットは元画像に合わせようとしたらエラーになったのでPbgra32に指定した、元画像はBgr32だった

f:id:gogowaten:20210124215918p:plain
切り抜いた画像3枚それぞれをRectに従ってDrawImageしたイメージ

f:id:gogowaten:20210124220121p:plain
DrawingVisualのContentBoundsのXとY値をを使って位置調整後のイメージ

f:id:gogowaten:20210124220235p:plain
DrawingVisualのContentBoundsのWidthとHeightを使ってRenderTargeBitmapのサイズ調整後が緑枠

画像の保存

f:id:gogowaten:20210124220739p:plain
画像の保存
png形式画像、ファイル名はcropped_に今の時間をつけただけ
f:id:gogowaten:20210124221129p:plain
保存された画像ファイル
f:id:gogowaten:20210124221210p:plain
cropped_18時58分09秒.png
できた!


ラーメンには豆腐、このことは今後も繰り返し主張していきたい
それはともかく9年ぶりってのは大きかったようで、食べ終わったときは手を合わせるとかじゃなくて、ショーシャンクの空にみたいになってた



今回の記事中の画像で使ったフォントは

f:id:gogowaten:20210124223413p:plain
ロックンロール One
直線的でスッキリした感じがいい!
Fontworks|フォントワークス
https://fontworks.co.jp/
フォントワークス8書体が無料公開!商用利用や埋め込みも可能で「Google Fonts」にも対応 | Game*Spark - 国内・海外ゲーム情報サイト
https://www.gamespark.jp/article/2021/01/19/105389.html



関連記事
次回WPF記事は4日後

gogowaten.hatenablog.com

約2年前
gogowaten.hatenablog.com
このときはPathGeometryを使って切り抜いて表示してから、その要素を画像として取得して保存していた。PathGeometryを使うから矩形以外にも楕円、ベジェ曲線やフォントの形とかも使えるから形状の柔軟性が高い
今回の方法だと矩形しかできないけど、表示しなくても保存できるっていう違いがある

4年前
gogowaten.hatenablog.com 画像として保存はこっちだった