質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.48%
C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

Windows Forms

Windows Forms(WinForms)はMicrosoft .NET フレームワークに含まれる視覚的なアプリケーションのプログラミングインターフェイス(API)です。WinFormsは管理されているコードの既存のWindowsのAPIをラップすることで元のMicrosoft Windowsのインターフェイスのエレメントにアクセスすることができます。

Q&A

解決済

2回答

9601閲覧

C♯にてBitmap-24bitをBitmap-8bitの254(減色)にさせたい

tride

総合スコア68

C#

C#はマルチパラダイムプログラミング言語の1つで、命令形・宣言型・関数型・ジェネリック型・コンポーネント指向・オブジェクティブ指向のプログラミング開発すべてに対応しています。

Visual Studio

Microsoft Visual StudioはMicrosoftによる統合開発環境(IDE)です。多種多様なプログラミング言語に対応しています。

OpenCV

OpenCV(オープンソースコンピュータービジョン)は、1999年にインテルが開発・公開したオープンソースのコンピュータビジョン向けのクロスプラットフォームライブラリです。

Windows Forms

Windows Forms(WinForms)はMicrosoft .NET フレームワークに含まれる視覚的なアプリケーションのプログラミングインターフェイス(API)です。WinFormsは管理されているコードの既存のWindowsのAPIをラップすることで元のMicrosoft Windowsのインターフェイスのエレメントにアクセスすることができます。

1グッド

0クリップ

投稿2020/01/10 07:13

編集2020/03/05 08:26

24bit の画像を 8bit (254 color) にする方法についてわからなくて困っております。

やりたいこととしては以下URLの内容の、固定パレット(24bit→8bit (245 color))+誤差拡散(Floyd-Steinberg)、もしくは__最適化パレット(24bit→8bit)+誤差拡散(Floyd-Steinberg)__で行いたいと考えています。
ただし、色数は255ではなく254です。

現状はとにかく減色させて8bitにと思って、OpenCvSharp3のCv2.Kmeansによる減色を試みたのですが、減色はできても8bitにはできずにてこずっております。

環境:WindowsForms
URL:[http://koujinz.cocolog-nifty.com/blog/2009/04/24bit-8bit-a879.html](http://koujinz.cocolog-nifty.com/blog/2009/04/24bit-8bit-a879.html

尚、単純に以下のCloneでPixelFormat.Format8bppIndexedを指定した場合、8bitにはできるものの、色数254の前に画質が荒すぎて酷かったので止めました。

C#

1Bitmap dstBmp = ImageToBmp.Clone(new Rectangle(0, 0, ImageToBmp.Width, ImageToBmp.Height), PixelFormat.Format8bppIndexed);

また、以下のようにGIFに変換してからbmpにする方法も試しましたが、最後ビットマップにしてもGif画像扱い(バイナリヘッダーを見て判断)だったので不可としました。

C#

1 public static Bitmap ConvertTo8bpp(Image img) 2 { 3 var ms = new System.IO.MemoryStream(); 4 img.Save(ms, System.Drawing.Imaging.ImageFormat.Gif); 5 ms.Position = 0; 6 return new Bitmap(img); 7 }

<Kmeansで試している内容>

C#

1 /// <summary> 2 /// Kmeans法による減色 3 /// </summary> 4 /// <param name="input">入力画像</param> 5 /// <param name="result">減色結果画像</param> 6 /// <param name="ClusterCount">クラスター値</param> 7 public static void Kmeans(Mat input, Mat result, int ClusterCount) 8 { 9 using (Mat points = new Mat()) 10 { 11 using (Mat labels = new Mat()) 12 { 13 using (Mat centers = new Mat()) 14 { 15 int width = input.Cols; 16 int height = input.Rows; 17 18 points.Create(width * height, 1, MatType.CV_32FC3); 19 centers.Create(ClusterCount, 1, points.Type()); 20 result.Create(height, width, input.Type()); 21 22 int i = 0; 23 for (int y = 0; y < height; y++) 24 { 25 for (int x = 0; x < width; x++, i++) 26 { 27 Vec3f vec3f = new Vec3f 28 { 29 Item0 = input.At<Vec3b>(y, x).Item0, 30 Item1 = input.At<Vec3b>(y, x).Item1, 31 Item2 = input.At<Vec3b>(y, x).Item2 32 }; 33 34 points.Set<Vec3f>(i, vec3f); 35 } 36 } 37 38 Cv2.Kmeans(points, ClusterCount, labels, new TermCriteria(CriteriaType.Eps | CriteriaType.MaxIter, 10, 1.0), 3, KMeansFlags.PpCenters, centers); 39 40 i = 0; 41 for (int y = 0; y < height; y++) 42 { 43 for (int x = 0; x < width; x++, i++) 44 { 45 int idx = labels.Get<int>(i); 46 47 Vec3b vec3b = new Vec3b(); 48 49 50 int tmp = Convert.ToInt32(Math.Round(centers.At<Vec3f>(idx).Item0)); 51 tmp = tmp > 255 ? 255 : tmp < 0 ? 0 : tmp; 52 vec3b.Item0 = Convert.ToByte(tmp); 53 54 tmp = Convert.ToInt32(Math.Round(centers.At<Vec3f>(idx).Item1)); 55 tmp = tmp > 255 ? 255 : tmp < 0 ? 0 : tmp; 56 vec3b.Item1 = Convert.ToByte(tmp); 57 58 tmp = Convert.ToInt32(Math.Round(centers.At<Vec3f>(idx).Item2)); 59 tmp = tmp > 255 ? 255 : tmp < 0 ? 0 : tmp; 60 vec3b.Item2 = Convert.ToByte(tmp); 61 62 result.Set<Vec3b>(y, x, vec3b); 63 64 } 65 } 66 } 67 } 68 } 69 70 }

追記1>
要件:
メディアンカットでの減色を行いたい(記載しているのはKmeans)
・24bit→8bit(254色)に変換したい
・上記に関するライブラリ、参考サイト、コードがあれば

現状:
メディアンカットでは進展なし。
24bit→8bit変換でサンプルを作成中。

追記2>
サンプルとして、以下のところまで作成。

NotionalKettleさん
つまり減色された画像データに含まれるRGBの組とそれに対応する値(8bit用)を定義して対応が付けられるように8bit側の各画素を上記の方法>で書き換えて、最後にパレットを対応させれば・・・

この部分でどうすればいいのかイメージがつかずに詰まってます。
下記コードではコメント①あたりです。
尚、byte*での方法については理解が追い付いていないので、とりあえずMarshal.Copyで試してます。

C#

1 2 unsafe static Bitmap ImageConvert24biTo8bit(Bitmap bmpSource) 3 { 4 int pixelSize = 4; 5 int color = 255; 6 7 //入力されたBitmapをロック 8 BitmapData bmpData = bmpSource.LockBits( 9 new Rectangle(0, 0, bmpSource.Width, bmpSource.Height), 10 ImageLockMode.WriteOnly, 11 bmpSource.PixelFormat 12 ); 13 14 //Bitmapの各画素情報のbyte配列データの先頭ポインタ 15 IntPtr pointer = bmpData.Scan0; 16 //byte* pImg = (byte*)bmpData.Scan0.ToPointer(); 17 18 byte[] pixels = new byte[bmpData.Stride * bmpSource.Height]; 19 20 // Bitmap を byte [ ] へコピー 21 Marshal.Copy(pointer, pixels, 0, pixels.Length); 22 23 // Bitmapオブジェクトを作成 24 Bitmap bmp8bit = new Bitmap(bmpSource.Width, bmpSource.Height, PixelFormat.Format8bppIndexed); 25 26 // カラーパレットを設定 27 ColorPalette pal = bmp8bit.Palette; 28 29 for (int y = 0; y < bmpData.Height; y++) 30 { 31 for (int x = 0; x < bmpData.Width; x++) 32 { 33 /*① この辺りが?です */ 34 35 //(x,y)のデータ位置 36 int pos = y * bmpData.Height + x * pixelSize; 37 38 // RGB 39 byte b = pixels[pos]; 40 byte g = pixels[pos + 1]; 41 byte r = pixels[pos + 2]; 42 43 // 色 44 for (int i = 0; i < color; ++i) 45 { 46 pal.Entries[i] = Color.FromArgb(255, r, g, b); 47 } 48 bmpSource.Palette = pal; 49 } 50 } 51 52 53 // 変更を反映 54 Marshal.Copy(pixels, 0, pointer, pixels.Length); 55 56 //ロックを解除する 57 bmpSource.UnlockBits(bmpData); 58 59 return bmpSource; 60 }

追記:
メディアンカットは本要件と違ったため、その部分を削除しました。
また追記部分には取り消し線で対応しました。

x_x👍を押しています

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

Wind

2020/01/10 07:55

BitmapはただのByte配列ですので、自分で計算するのが一番シンプルで画質も自分好みに出来るのでは無いでしょうか? https://www.setsuki.com/hsp/ext/bmp.htm
guest

回答2

0

ベストアンサー

内容が膨れ上がって来たので別回答で失礼します。

追加編集された内容について、正直何書いていいか困ってます...。
とりあえずやりたいことを整理して、1個1個やりましょう。

まずメディアンカット法については調べた感じでは原理は単に色空間の量子化を粗くするだけみたいなのでまぁこれは自力でやってもらわないと困る話かなと...。
今回は特にメディアンカットを中心に綺麗に減色するのではなくk-meansの軽量化の話っぽいのであれこれ考えず少し粗くすればよいだけと思うので。

で、ソースの方の問題点ですが
0. pixelSizeは4ではない(恐らく32bitのサンプルでも見たのでは?)

  1. 24bitBmpからはデータも取得するのでWriteOnlyではない
  2. posはy * bmpData.Height + x * pixelSizeではなくy * bmpData.Stride + x * pixelSize
  3. パレットの扱いが滅茶苦茶(多分パレットの意味が分かっていなと思います)

とりあえず1個1個やりましょう...
まずはパレットは置いておいて24bit画像のRGBへのアクセスだけをやってみてください。
それが出来たらメディアンカット法ができるはずです。ここまではパレットも8bitも気にせず24bitの画像だけで処理してください。
それが出来たらOpenCVでK-meansしてください。
それが出来て初めて24bitから8bitへの変換です。とりあえずパレットとはどういうものかについて調べてみて良く分からなかったらまたコメントください。

あと、質問を編集しただけでは私には通知が来ないようなので回答にコメントしてもらえると反応が早いかもしれません。

【コメントを受け追記】
減色済みの奴が対象で減勘だけするならこんな感じで良いはず。
例外処理とか諸々省略してるので注意。

C#

1static Bitmap ImageConvert24bitTo8bit(Bitmap bmpSource) 2{ 3 Bitmap bmp8bit = new Bitmap(bmpSource.Width, bmpSource.Height, PixelFormat.Format8bppIndexed); 4 5 BitmapData bmpData8bit = bmp8bit.LockBits( 6 new Rectangle(0, 0, bmp8bit.Width, bmp8bit.Height), 7 ImageLockMode.WriteOnly, 8 bmp8bit.PixelFormat 9 ); 10 11 BitmapData bmpData24bit = bmpSource.LockBits( 12 new Rectangle(0, 0, bmpSource.Width, bmpSource.Height), 13 ImageLockMode.ReadOnly, 14 bmpSource.PixelFormat 15 ); 16 17 byte[] imgBuf8bit = new byte[bmpData8bit.Stride * bmp8bit.Height]; 18 byte[] imgBuf24bit = new byte[bmpData24bit.Stride * bmpSource.Height]; 19 20 Marshal.Copy(bmpData24bit.Scan0, imgBuf24bit, 0, imgBuf24bit.Length); 21 22 var colors = new Dictionary<Color, byte>(); 23 byte colorIndex = 0; 24 25 for (int y = 0; y < bmpSource.Height; y++) 26 { 27 for (int x = 0; x < bmpSource.Width; x++) 28 { 29 int index24bit = y * bmpData24bit.Stride + x * 3; 30 int index8bit = y * bmpData8bit.Stride + x; 31 byte b = imgBuf24bit[index24bit]; 32 byte g = imgBuf24bit[index24bit + 1]; 33 byte r = imgBuf24bit[index24bit + 2]; 34 35 // 24bitから8bitへ 36 var color = Color.FromArgb(r, g, b); 37 if (colors.TryGetValue(color, out byte i)) 38 imgBuf8bit[index8bit] = i; 39 else 40 { 41 imgBuf8bit[index8bit] = colorIndex; 42 colors.Add(color, colorIndex); 43 colorIndex++; 44 } 45 } 46 } 47 Marshal.Copy(imgBuf8bit, 0, bmpData8bit.Scan0, imgBuf8bit.Length); 48 bmpSource.UnlockBits(bmpData24bit); 49 bmp8bit.UnlockBits(bmpData8bit); 50 51 var pal = bmp8bit.Palette; 52 foreach (var item in colors) 53 pal.Entries[item.Value] = item.Key; 54 bmp8bit.Palette = pal; 55 56 return bmp8bit; 57}

投稿2020/01/12 13:59

編集2020/01/13 03:03
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

tride

2020/01/13 00:07

お返事ありがとうございます。 24bit-8bit変換のほうが最優先でメディアンカットは後回しでもよかったですが、RGBへのアクセスについてまるで頭の中にないので、そこから進めたいと思います。
退会済みユーザー

退会済みユーザー

2020/01/13 01:58

回答に困った理由がここにあって、変換するにしても先に減色されていることが前提でないとパレットが作れないんです。 ソースコードを見てちょっと減色済みの物を前提にしているのか、そこで減色もできると思ってしまっているのか判断がつかなかったので・・・
tride

2020/01/13 02:14

追記したコードに関しては、すでに記載していたKmeansで減色させたものを前提に書きました。 なので、流れとしてはKmeans→ImageConvert24biTo8bitのつもりです。 ただ、自分でも色々とごちゃごちゃしていて、最初はメディアンカットと8bit変換が一緒にできると考えていたので、追記コードがごちゃごちゃとややこしいことになってたり。
tride

2020/01/13 05:07

ありがとうございます! コードを見てようやく理屈を把握できました。 特にコメントにある[//24bitから8bitへ]の辺りですっきりです。 私の考え方自体が違っていました。 ただ一つ気になったのは、254色にする場合について、以前の回答にあった以下の部分で、パレットの各色は画素値に対応している為にソースでも255色入れているという理解でいいでしょうか。 また、その場合は減色さえしていればわざわざ254にしなくてもいいということでしょうか。 >値は先頭からColor.FromArgb(i,i,i)でiを0から255までです。 >今回254色とのことですが、パレットの各色は画素値に対応しているので>255まで入れます。
退会済みユーザー

退会済みユーザー

2020/01/13 06:18

それに関してはちょっと私の方が途中から(最初の質問忘れて)グレースケールと勘違いしていたため0, 0, 0から255, 255, 255まで入れると言ってしまっていただけというのがあるので過去の回答の該当部は気にしないでください。 要は画素値(8bitで表現できる各画素の値)と対応するパレット(画素値をindexとしたときに対応する色が得られる並び)が用意できていれば大丈夫です。
tride

2020/01/14 06:19

承知しました。 メディアンカットについては、今回は本文を修正して無しとし、bit変換ができたという事で、当回答をベストアンサーに選びたいと思います。
guest

0

【過去の内容】
C#でBitmapこのフォーマット変換をするのは実は結構しんどいです。
まぁ下記の機能を実装したライブラリは探せば見つかると思うので探した方が早いとおもいますが参考までに。

減色方法は好きなようにやっていただくとして、24bitから8bitへの変換ですが
Bitmap.LockbitsでBitmapDataを取得し、BitmapData.Scan0を得ます。
これはBitmapの各画素情報のbyte配列データの先頭ポインタを表します。

次に24bitBitmapのデータの並びを意識しながらポインタでアクセスします。
RGBデータが各行BGRBGR...の順で並んでいるのでここで減色処理を書くこともできます。
この時各行のbyte数が4の倍数にアライメントされているので気をつけて下さい。
例えば画像幅が5だとするとBGRが5つで15、4の倍数になるように未使用の領域が1byte加わって各行16byteになります。
画像高さについては特に気にする必要はなく、通常通り処理して問題ありません。

同様にサイズだけ指定した空の8bitのBitmapを用意しポインタで画素にアクセスします。
8bitの場合はRGBではないので各byteデータが1画素を指していますが、やはり4byteでアライメントされているのでそこだけ気をつけて下さい。
このように24bitと8bitの各画素にアクセスしながら値を変換して移していけばフォーマットの変換はほぼ完了です。

最後に値を入れた8bitBitmapのパレットを設定してやります。
打ち消し線
値は先頭から~~Color(i,i,i)~~Color.FromArgb(i,i,i)でiを0から255までです。
今回254色とのことですが、パレットの各色は画素値に対応しているので255まで入れます。
(ですのでカラーの256色8bitBitmapというのもできたりします)

あとは保存すれば完了です。

【訂正】
途中から勝手にグレースケールの254色と勘違いしてたので訂正入れます。
減色自体はできたということなので最後のパレット部分を上手いことやってやればいいんじゃないかと思います。
つまり減色された画像データに含まれるRGBの組とそれに対応する値(8bit用)を定義して対応が付けられるように8bit側の各画素を上記の方法で書き換えて、最後にパレットを対応させれば行けると思います。

.Cloneによる方法は正直よく分からないです。
減色済みのやつに対して上手にパレットを生成するような内部実装になっていないんでしょうね...

投稿2020/01/10 08:58

編集2020/01/11 03:16
退会済みユーザー

退会済みユーザー

総合スコア0

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

退会済みユーザー

退会済みユーザー

2020/01/10 09:07

補足: ・終わったらLockbitsと対のUnlockBitsが必要です。 ・Scan0はIntPtrなのでToPointerとキャストでbyte*にします。 ・ポインタを扱うのでunsafe宣言が必要です。 ・さらにプロジェクト設定でアンセーフコードを許可する必要があります。 GetPixelとSetPixelを使えば回答のような大変な記述は不要ですが、その代わり物凄く遅いです。
退会済みユーザー

退会済みユーザー

2020/01/10 13:25 編集

確かにマネージドバッファにコピーする手もありますが、それによって軽減できる面倒というのがあんまりなかったりします。 byte*にするのも byte* pImg = (byte*)bmpData.Scan0.ToPointer(); で1行ですし、unsafe宣言とチェック入れるのも大した手間ではないです。 処理的には無駄なコピーが2回増えるしマネージドバッファの用意&Marshal.Copyで記述も減ってるか微妙な所。unsafeを回避したい目的でなければさほど意味はないと思います。 マネージドでないのでIndexOutOFRangeExceptionをthrowしてくれない問題はありますが、各画素を1回走査する程度の簡単な処理ならそこまでハイリスクでもないかなと(ここは程度と好みの問題あると思います) いずれにしてもLockBitsしたりBitmapの色の並びを意識したりアライメント気にしたりパレット弄ったり全体的には面倒なんですよね... Indexedでないフォーマット間の変換(ex. 32bit⇔24bit)ならGraphicsで書いちゃえば簡単なんですが
Q71

2020/01/10 23:13

(byte)(bitmapData.Scan0.ToInt32() & 0xff)
退会済みユーザー

退会済みユーザー

2020/01/11 02:27

ポインタの下8bitをbyte型にしても意味はないですねー。それ画素値じゃないですし。
Q71

2020/01/11 04:59

ごめんなさい。違う。あれ?unsafe、MarshalCopyしなくても画素値にアクセスできるけど、出てこない...
Zuishin

2020/01/11 05:12

GetPixel でできたと思いますけど、遅いんですよね。
退会済みユーザー

退会済みユーザー

2020/01/11 05:43

対象データのbyte数分メソッド呼び出すのでやっぱり遅いんじゃないかと...unsafe回避するならimihitoさんの仰るMarshal.Copyで一旦マネージドバッファにコピーするのがベストと思います。 >GetPixel でできたと思いますけど、遅いんですよね。 GetPixelとかスポイト機能くらいしか使う場面なさそうですよね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.48%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問