2011-01-27 19 views
11

Załóżmy, że mam System.Drawing.Bitmap w trybie ARb 32bpp. Jest to duża bitmapa, ale w większości są to przezroczyste piksele ze stosunkowo małym obrazem w środku.Automatyczne przycinanie bitmapy do minimalnego rozmiaru?

Co to jest szybki algorytm do wykrywania granic "prawdziwego" obrazu, więc mogę usunąć wszystkie przezroczyste piksele z niego?

Czy istnieje funkcja już dostępna w .Net, której mogę użyć do tego?

+2

Czy odcięcie jest proste? jeśli tak, odczyt pikseli z L-> R i T-> B działałby bardzo szybko. –

+0

Jeśli jest kwadratowy, prawdopodobnie możesz zaoszczędzić jeszcze więcej czasu i binarnego wyszukiwania od środka na wszystkich 4 stronach (przynajmniej zmniejszając piksele). –

+0

Czy mały, osadzony obraz może zawierać przezroczyste piksele? –

Odpowiedz

23

Podstawową ideą jest sprawdzenie każdego piksela obrazu, aby znaleźć górne, lewe, prawe i dolne krawędzie obrazu. Aby to zrobić wydajnie, nie używaj metody GetPixel, która jest dość powolna. Zamiast tego użyj LockBits.

Oto realizacja wymyśliłem:

static Bitmap TrimBitmap(Bitmap source) 
{ 
    Rectangle srcRect = default(Rectangle); 
    BitmapData data = null; 
    try 
    { 
     data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); 
     byte[] buffer = new byte[data.Height * data.Stride]; 
     Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); 
     int xMin = int.MaxValue; 
     int xMax = 0; 
     int yMin = int.MaxValue; 
     int yMax = 0; 
     for (int y = 0; y < data.Height; y++) 
     { 
      for (int x = 0; x < data.Width; x++) 
      { 
       byte alpha = buffer[y * data.Stride + 4 * x + 3]; 
       if (alpha != 0) 
       { 
        if (x < xMin) xMin = x; 
        if (x > xMax) xMax = x; 
        if (y < yMin) yMin = y; 
        if (y > yMax) yMax = y; 
       } 
      } 
     } 
     if (xMax < xMin || yMax < yMin) 
     { 
      // Image is empty... 
      return null; 
     } 
     srcRect = Rectangle.FromLTRB(xMin, yMin, xMax, yMax); 
    } 
    finally 
    { 
     if (data != null) 
      source.UnlockBits(data); 
    } 

    Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height); 
    Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height); 
    using (Graphics graphics = Graphics.FromImage(dest)) 
    { 
     graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel); 
    } 
    return dest; 
} 

może prawdopodobnie być zoptymalizowane, ale nie jestem GDI + ekspertem, więc jest to najlepsze, co mogę zrobić bez dalszych badań ...


EDIT: rzeczywiście, jest to prosty sposób, aby zoptymalizować go, by nie skanuje niektóre części obrazka:

  1. skanowania lewej do righ t dopóki nie znajdziesz nieprzejrzystego piksela; zapisz (x, y) na (xMin, yMin)
  2. skanuj od góry do dołu, aż znajdziesz nieprzezroczysty piksel (tylko dla x> = xMin); przechowuj y na yMin
  3. skanuj od prawej do lewej, aż znajdziesz nieprzezroczysty piksel (tylko dla y> = yMin); przechowuj x na xMax
  4. skanuj od dołu do góry, aż znajdziesz nieprzezroczysty piksel (tylko dla xMin < = x < = xMax); sklep y do YMAX

Edit2: oto implementacja podejścia powyżej:

static Bitmap TrimBitmap(Bitmap source) 
{ 
    Rectangle srcRect = default(Rectangle); 
    BitmapData data = null; 
    try 
    { 
     data = source.LockBits(new Rectangle(0, 0, source.Width, source.Height), ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); 
     byte[] buffer = new byte[data.Height * data.Stride]; 
     Marshal.Copy(data.Scan0, buffer, 0, buffer.Length); 

     int xMin = int.MaxValue, 
      xMax = int.MinValue, 
      yMin = int.MaxValue, 
      yMax = int.MinValue; 

     bool foundPixel = false; 

     // Find xMin 
     for (int x = 0; x < data.Width; x++) 
     { 
      bool stop = false; 
      for (int y = 0; y < data.Height; y++) 
      { 
       byte alpha = buffer[y * data.Stride + 4 * x + 3]; 
       if (alpha != 0) 
       { 
        xMin = x; 
        stop = true; 
        foundPixel = true; 
        break; 
       } 
      } 
      if (stop) 
       break; 
     } 

     // Image is empty... 
     if (!foundPixel) 
      return null; 

     // Find yMin 
     for (int y = 0; y < data.Height; y++) 
     { 
      bool stop = false; 
      for (int x = xMin; x < data.Width; x++) 
      { 
       byte alpha = buffer[y * data.Stride + 4 * x + 3]; 
       if (alpha != 0) 
       { 
        yMin = y; 
        stop = true; 
        break; 
       } 
      } 
      if (stop) 
       break; 
     } 

     // Find xMax 
     for (int x = data.Width - 1; x >= xMin; x--) 
     { 
      bool stop = false; 
      for (int y = yMin; y < data.Height; y++) 
      { 
       byte alpha = buffer[y * data.Stride + 4 * x + 3]; 
       if (alpha != 0) 
       { 
        xMax = x; 
        stop = true; 
        break; 
       } 
      } 
      if (stop) 
       break; 
     } 

     // Find yMax 
     for (int y = data.Height - 1; y >= yMin; y--) 
     { 
      bool stop = false; 
      for (int x = xMin; x <= xMax; x++) 
      { 
       byte alpha = buffer[y * data.Stride + 4 * x + 3]; 
       if (alpha != 0) 
       { 
        yMax = y; 
        stop = true; 
        break; 
       } 
      } 
      if (stop) 
       break; 
     } 

     srcRect = Rectangle.FromLTRB(xMin, yMin, xMax, yMax); 
    } 
    finally 
    { 
     if (data != null) 
      source.UnlockBits(data); 
    } 

    Bitmap dest = new Bitmap(srcRect.Width, srcRect.Height); 
    Rectangle destRect = new Rectangle(0, 0, srcRect.Width, srcRect.Height); 
    using (Graphics graphics = Graphics.FromImage(dest)) 
    { 
     graphics.DrawImage(source, destRect, srcRect, GraphicsUnit.Pixel); 
    } 
    return dest; 
} 

Nie będzie znaczny zysk, jeśli nieprzezroczysta część jest mały oczywiście od nadal będzie skanował większość pikseli. Ale jeśli jest duży, skanowane będą tylko prostokąty wokół nieprzejrzystej części.

+0

Wydaje się najbardziej praktycznym podejściem. Nie widzę, żeby to było lepsze. Dobra wskazówka również dzięki metodzie LockBits. +1 –

+2

BTW, właśnie zdałem sobie sprawę, że istnieje prostszy sposób przycinania obrazu bez użycia grafiki: 'return source.Clone (srcRect, source.PixelFormat);' –

+2

Świetne rozwiązanie, bardzo pomocne, ale znalazłem moje obrazy obcinanie o jeden piksel za dużo. Logicznie wydaje się, że twój, ale zmieniłem połączenie na ** Rectangle.FromLTRB ** na ** srcRect = Rectangle.FromLTRB (xMin, yMin, xMax + 1, yMax + 1) ** i teraz działa idealnie. –

1

chciałbym zaproponować podział podejście & przejęcie:

  1. podzielić obraz na środku (nppionowo)
  2. sprawdź czy są nieprzejrzyste pikseli na linii cięcia (jeśli tak, należy pamiętać, Min/Max dla pola)
  3. podziału lewa połowa ograniczające ponownie pionowo
  4. jeśli linia cięcia zawiera nieprzezroczyste piksele -> aktualizuj ramkę ograniczającą
  5. jeśli nie, prawdopodobnie odrzuć lewą połowę (nie wiem zdjęć)
  6. Kontynuuj z lewą-prawą połówką (stwierdziłeś, że obraz jest gdzieś pośrodku), dopóki nie znajdziesz skrajne skrajne z lewej strony obrazu robią to samo dla prawej połówki
  7. zrobić to samo dla prawej połowy
+3

Myślę, że twój 5 punkt jest błędny: może istnieć kilka różnych obszarów z nieprzezroczystymi pikselami, więc fakt, że nie ma nieprzejrzystego piksela na linii cięcia, nie oznacza niczego. –

+0

Dzięki bjoernz, ale tak: wyszukiwanie binarne będzie nie zawsze działa dla moich obrazów - możliwe, że są na przykład dwa obrazy oddzielone białymi znakami. – Blorgbeard

Powiązane problemy