Oříznout obrázek   otázka

VB.NET

Pěkný poslední únorový den všem,

v souborech je uložen náhled, *.png .

Obrázek je na bílém pozadí a dost často je na velkém plátně

docela malý obrázek, který potřebuji nějak najít a vyříznout.

Teoreticky snad chápu, prakticky tápu , zřejmě se prochází řádky

a sloupce shora, zdola, zleva a zprava a kontroluje se dokud jsou

v řádku či sloupci všechny pixely bílé, ze všech stran a nakonec

se patřičně ořízne do nové image...

Jak pomocí VB.net oříznu bílé okraje k získání nové oříznuté image?

Jak otevřít png jako bitmap? Format24bppRgb?

Jak pracovat s LockBits a UnlockBits? Rychlý způsob, jak na to ?

Nějaký algoritmus? Diagonálně? Od středu ven? Pixel po pixelu? Matrix?

Nemáte prosím nějaký tip či zkušenost.

Děkuji za případnou pomoc.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Pro obraznost jde o něco na tento způsob, všimnul jsem si v druhém vlákně, že je potřeba to patřičně rozvést... ;)

Vypadá, že hodnoty pro každý pixel (24bit/pixel) jsou v poli za sebou r,b,g první , pak r,b,g druhý atd.

Nepotřebuji ořízlou image ukládat, jde mi pouze o to za běhu zobrazit z obrázku maximum na minimálním prostoru v přiměřeném čase.

Prvopokusy trvaly nekonečně dlouho, tohle je trochu lepší.

A tak přikládám pokus s ořezáním obrázku v pictureboxu :

Sub CropImage(ByVal pb As PictureBox)
        Dim sw As Stopwatch = Stopwatch.StartNew
        Dim bmp As Bitmap = pb.Image
        Dim rect As New Rectangle(0, 0, bmp.Width, bmp.Height)
        Dim bmpData As System.Drawing.Imaging.BitmapData = bmp.LockBits(rect, System.Drawing.Imaging.ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb)

        ' Get the address of the first line.
        Dim ptr As IntPtr = bmpData.Scan0
        ' Declare an array to hold the bytes of the bitmap.
        ' This code is specific to a bitmap with 24 bits per pixels.
        Dim bytes As Integer = bmp.Width * bmp.Height * 3
        Dim rgbValues As Byte() = New Byte(bytes - 1) {}
        ' Copy the RGB values into the array.
        System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, bytes)
        bmp.UnlockBits(bmpData)

        Dim points As New List(Of Point)
        'Add points of all colors except white.
        For y As Integer = 0 To bmp.Height - 1
            For x As Integer = 0 To bmp.Width - 1
                ' Process the pixel's bytes.
                Dim r, b, g As Byte
                Dim i As Integer = (x + y * bmp.Width) * 3
                r = rgbValues(i)
                b = rgbValues(i + 1)
                g = rgbValues(i + 2)
                If Not (r = 255 And b = 255 And g = 255) Then
                    points.Add(New Point(x, y))
                End If
            Next x
        Next y

        If Not points.Count > 0 Then Exit Sub
        'Get bounding box for list of points.
        Dim rct As Rectangle = BoundingBox(points)
        'Crop original image.
        Dim bmpc = New Bitmap(rct.Width, rct.Height)
        Using g As Graphics = Graphics.FromImage(bmpc)
            g.DrawImage(pb.Image, 0, 0, rct, GraphicsUnit.Pixel)
        End Using
        pb.Image = bmpc

        sw.Stop()
        Debug.Print("Finished at " & sw.Elapsed.TotalSeconds.ToString("0.0s"))
        sw = Nothing
    End Sub

Myslím si, že je to lepší, než čtyři cykly z každé strany, jak jsem psal výše.

Inspirováno dle link pod kódem, pro vb.net by to mohlo vypadat nějak takhle:

' Find the list's bounding box.
    Private Function BoundingBox(ByVal points As IEnumerable(Of Point)) As Rectangle
        Dim x_query = From p In points Select p.X
        Dim xmin As Integer = x_query.Min()
        Dim xmax As Integer = x_query.Max()

        Dim y_query = From p In points Select p.Y
        Dim ymin As Integer = y_query.Min()
        Dim ymax As Integer = y_query.Max()

        Return New Rectangle(xmin, ymin, xmax - xmin, ymax - ymin)
    End Function

Inspirace dle C# linq tip jak najít obdélník, který orámuje všechny nalezené body :

http://csharphelper.com/blog/2014/11/use...

Nejsem si jist, doufám, že mám LockBits a UnlockBits použito správně, možná existuje i jiný způsob? Takhle by mělo být stále rychlejší než pomocí GetPixel(x,y).

Obrázky 640x480 mi ořízne v rozmezí 0,2-0,4s zřejmě v závislosti na počtu nebílých bodů.

Vaše postřehy a připomínky vítány.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Ještě přidám marný linq pokus s výsledkem, že trvá 10x déle :

Dim points As IEnumerable(Of Point) = _
                    From x As Integer In Enumerable.Range(0, bmp.Width) _
                    From y As Integer In Enumerable.Range(0, bmp.Height) _
                    Where Not (rgbValues(x * 3 + y * bmp.Width * 3) = 255 _
                                    And rgbValues(x * 3 + y * bmp.Width * 3 + 1) = 255 _
                                    And rgbValues(x * 3 + y * bmp.Width * 3 + 2) = 255)
                    Select New Point(x, y)
nahlásit spamnahlásit spam 0 odpovědětodpovědět

Osobně bych šel asi cestou nějakého fíglu. Za předpokladu že je obrázek čtvercový, pokaždé uprostřed a na bílém plátně, zkusil bych zjistit veškeré strany formou "vlákna zaměřovače" :).

Prostě bych hledal ty bílé pixely vždy v polovině obrazu (jak vertikálně tak horizontálně) a získal bych jednou iteraci z každé strany min/max koordinát posledního bílého pixelu.

Tzn. ještě jednou, testoval bych bílé pixely pouze v H/V ose obrázku, pro opatrnost bych přidal ještě ten samý test v odstupu třeba 10 pixelů v/h od každé osy na obě strany.

Celkem 3 průchody V a 3 průchody H. Objem testovaných pixelů by byl markatně jinde, než při testování všech pixelů.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Na čtverec jsem ještě nenarazil.. :) Vím, že to myslíte obrazně.

Někdy jsou v plátně i dva obrázky, po oříznutí každý ve svém rohu, těžko vymyslet něco spolehlivého, dokonce jsem ještě uvažoval o něčem, co by mizivě malou oblast pixelů ignorovalo a ořízlo pouze velkou oblast (bez té malé hnidy někde v rohu). Takže místo fíglu ještě další složitost...

A další případ je několik obrázků, někdy umístěných v mřížce...

U velkých obrázků je to přirozeně znát a zpracování je mnohem delší.

Zkoušel jsem nejdřív jít cestou s původním záměrem s cyklem z každé strany, pro obrázek s malým oříznutím bylo velmi rychlé, ale pro malou hnidu někde v plátně trvalo neskutečně dlouho ve srovnání s uvedenou verzí.

Děkuji za reakci, nějaký takový fígl by vše celkově urychlil, dost možná existují nějaké profesionální nástroje... Každopádně je milé, když se aspoň někdo ozve.

nahlásit spamnahlásit spam 0 odpovědětodpovědět

dobrá tedy, podrobněji jsem se na to vaše řešení podíval. Píšete 0,2-0,3 sekundy na průchod 640x480 obrázku. To je dost rychlé ne?

Co jsem se díval tak Vaše řešení stojí na locknutí bitmapy (LockBits), zkopírování obsahu do byte pole a následné analýzy.

Co takhle zkrátit to o to kopírování do byte pole, a rovnou během locknutí bitmapy testovat pixely a sbírat si je pokud nejsou bílé.

Je to to samé jen se to děje už dříve. A co jsem zatím zjistil, tak během locknutí bitmapy by operace nad ní, mimo jiné jako je třeba GetPixel, měli fungovat rychleji.

Něco jako:

Dim imageData As BitmapData = image.LockBits(New Rectangle(0, 0, image.Width, image.Height), ImageLockMode.ReadWrite, PixelFormat.Format24bppRgb)
Dim bytesForPixel As Integer = 3

Dim PixelComponent As Pointer(Of Byte) = CType(imageData.Scan0, Pointer(Of Byte))
For y As Integer = 0 To imageData.Height - 1
	For x As Integer = 0 To imageData.Width - 1
		' Get color components (watch out for order!)
		Dim pixelB As Byte = System.Math.Max(System.Threading.Interlocked.Increment(PixelComponent),PixelComponent - 1).Target
		If Not pixelB=255 Then 
			Dim pixelG As Byte = System.Math.Max(System.Threading.Interlocked.Increment(PixelComponent),PixelComponent - 1).Target
			If Not pixelG=255 Then 
				Dim pixelR As Byte = PixelComponent.Target
				If Not pixelR=255 Then points.Add(New Point(x, y))
			End If
		End If
	Next
Next
image.UnlockBits(imageData)

'z https://www.codeproject.com/Articles/406045/Why-the-use-of-GetPixel-and-SetPixel-is-so-ineffic

Taky si změřte, co je rycheljší a co zdržuje, jestli je to hledání v poli z bitmapy anebo vlastní generování bitmapy z těch bodů. Na toto používám System.Diagnostics.Stopwatch

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Udělal jsem demo z Vašeho kodu jen s drobnými úpravami:

Imports System.Drawing.Imaging

Public Class Form1
    Private Sub btnCrop_Click(sender As Object, e As EventArgs) Handles btnCrop.Click
        Dim sw As Stopwatch = Stopwatch.StartNew
        Dim points As New List(Of Point)

        Dim tmpImage As Bitmap = PictureBox1.Image.Clone
        Dim imageData As BitmapData = tmpImage.LockBits(New Rectangle(0, 0, tmpImage.Width, tmpImage.Height), ImageLockMode.ReadOnly, PixelFormat.Format24bppRgb)
        Dim bytesForPixel As Integer = 3


        Dim PixelComponent As IntPtr = imageData.Scan0

        Dim maxBytes As Integer = (Math.Abs(imageData.Stride) * tmpImage.Height)
        Dim itCnt As Integer = 0

        'Dim tmpArray(maxBytes - 1) As Byte
        'Runtime.InteropServices.Marshal.Copy(PixelComponent, tmpArray, 0, maxBytes)
        For y As Integer = 0 To imageData.Height - 1
            For x As Integer = 0 To imageData.Width - 1
                'Get no white pixels
                'Debug.Print("Iterace " & itCnt & " value:" & Runtime.InteropServices.Marshal.ReadByte(imageData.Scan0 + itCnt))
                Dim i As Integer = (x + y * tmpImage.Width) * 3
                'Debug.Print("R:" & Runtime.InteropServices.Marshal.ReadInt32(PixelComponent) & "G:" & Runtime.InteropServices.Marshal.ReadInt32(PixelComponent + 1) & "B:" & Runtime.InteropServices.Marshal.ReadInt32(PixelComponent + 2))

                If Runtime.InteropServices.Marshal.ReadByte(PixelComponent + i) <> 255 Then
                    If Runtime.InteropServices.Marshal.ReadByte(PixelComponent + i + 1) <> 255 Then
                        If Runtime.InteropServices.Marshal.ReadByte(PixelComponent + i + 2) <> 255 Then
                            points.Add(New Point(x, y))
                        End If
                    End If
                End If
                itCnt += 1
            Next
            'If (PixelComponent + imageData.Stride).ToInt32 <= (PixelComponent + maxBytes).ToInt32 Then PixelComponent = PixelComponent + 2
        Next

        Debug.Print("Drop white pixels: " & sw.ElapsedMilliseconds.ToString & "ms. Get pixels:" & points.Count)
        sw.Restart()

        If points.Count > 0 Then
            'Crop
            'Get bounding box for list of points.
            Dim rct As Rectangle = BoundingBox(points)

            Debug.Print("Get new size: " & sw.ElapsedMilliseconds.ToString & "ms.")
            sw.Restart()

            'Crop original image.
            Dim bmpc = New Bitmap(rct.Width, rct.Height)
            Using g As Graphics = Graphics.FromImage(bmpc)
                g.DrawImage(PictureBox1.Image, 0, 0, rct, GraphicsUnit.Pixel)
            End Using
            PictureBox2.Image = bmpc

            sw.Stop()
            Debug.Print("Create new picture: " & sw.Elapsed.TotalMilliseconds.ToString)
            sw = Nothing

            Debug.Print("Original size:" & tmpImage.Width & " x " & tmpImage.Height)
            Debug.Print("Destation size:" & bmpc.Width & " x " & bmpc.Height)
        End If
        tmpImage.UnlockBits(imageData)
    End Sub


    ''' <summary>Find the list's bounding box.</summary>
    Private Function BoundingBox(ByVal points As IEnumerable(Of Point)) As Rectangle
        Dim x_query = From p In points Select p.X
        Dim xmin As Integer = x_query.Min()
        Dim xmax As Integer = x_query.Max()

        Dim y_query = From p In points Select p.Y
        Dim ymin As Integer = y_query.Min()
        Dim ymax As Integer = y_query.Max()

        Return New Rectangle(xmin, ymin, xmax - xmin, ymax - ymin)
    End Function

End Class

A vysledky jsou vcelku dobré:

Drop white pixels: 62ms. Get pixels:40763

Get new size: 3ms.

Create new picture: 2,1466

Original size:640 x 480

Destation size:420 x 342

Takže pokud je to v rámci LockBits tak se člověk dostane na čas okolo 70ms... to je skvělé ne? Rychleji to asi nepůjde bez další nějaké optimalizace (třeba rastr ob řádku a sloupec při testování té bílé = výsledek by měl být o dalších 50% lepší).

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Mno a ještě jsem našel toto: https://www.codeproject.com/articles/617...

Shrnuli to vše jde o následující při operací per pixel:

- používat Lock/UnlockBits nad bitmapou

- kopírovat obsah paměti obrázku pomocí Marshal.Copy do pole (přesun dat z unmanaged oblasti do .net managed)

- dělat toho co nejméně (z hlediska deklarací, volání a používání dalších funkcí, psát co nejvýkonější kód - nemusí být nutně nejkratší :) )

- VB.NET bohužel neumí tkz.unsafe code (http://stackoverflow.com/questions/59158...

- výkon pomocí výše uvedeného je zřejmě limitem GDI+

- jediné řešení je ještě možná v paralelismu pomocí Tasks, které se docela dobře používá a oblast obrázku by šlo třeba rozdělit na jednotlivé 1/4 a ty počítat samostaně.

Podle testů je zjištění výsledného čtverce velice rychlé, proto jej lze spustit podle počtu výsledků z jednotlivých vláken a ty dílčí výsledky nakonec vhodně zkombinovat.

- koukal jsem se ještě na matematiku detekce hran, což by mohla být cesta pro Vás, ale metoda stejně vychází z per pixel operace

- a ještě pozn. skutečně si lze pohrát od jisté velikosti obrázku s velikostí kroku řádku a sloupce a dosáhnout tím vyšší výkonnosti. Pro detekci začátku obrázku v bílé oblasti stačí krok klidně ob 2 nebo ob 5 sloupců/řádků. Leda že by z hledaných obrázků vystřelovala nějaká linie, která by se nezachytila...

Přeji úspěšné laborování

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Dnes jsem objevil, že jste se do toho pěkně ponořil..

Díky za tip zachytávání bodů v locknuté bitmapě bez kopírování do pole.

Zkoumal jsem rozdíl použiji-li Marshal.Copy nebo Marshal.RedByte, časově je to relativně podobné. Měření je ošidné, dokonce i stejná operace stejným způsobem vrací pokaždé trochu rozdílné výsledky (rozdíl pár ms). Porovnávat to nelze, výsledky jsou takřka stejné. Kopie do pole je maličko rychlejší! :) :D

Ještě prozkoumám má-li vliv vytvoření temp kopie image pomocí Clone, ve skutečnosti však nenačítám image z pictureboxu, ale ze substreamu uloženém v souboru, který obsahuje obrázek coby náhled.

Koukal jsem na ty hrátky s odebráním růžové podprsenky :) a rozdělení akce do čtyř úloh ,zkusím.

Díky za ten link a taky za přání, pustím do laborování, dokud ty kočky nebudou nahoře bez... :)

Prvokusy byly v řádu několika vteřin, než jsem se propracoval k pár desetinám, netušíc, že budu řešit milisekundy... :) Pěkně děkuji! ;)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Právě z výše uvedeného linku na codeproject jsem získla takový pocit, že to s tím výkonem nahoru už moc nepůjde (z .NET a o to víc z VB.NET).

To svlékání podprdy byl vcelku efektní příklad, ale je to stejný postup jaký jste použil i vy. Pán to tam sice ještě trošku rozebral, ale nahoru(resp.dolů) to už podle všeho nepůjde.

Vy, já i on použil to nejlepší co .NET umí. Zbývá podle mě ten paralerní běh. Co s TPL knihovnou mám zkušenosti, vytáhla mě už mnohokrát z bryndy. Práce nad rozsekanými sektory obrázku se dají pěkně rozpočítat a definovat od určité velikosti, to říkám proto, že ten výkon škáluje jen od určité úrovně (myslím velikosti obrázku). Musí to mít smysl a musí to být vyvážené, ona ta příprava vláken a režie taky něco zabere.

https://msdn.microsoft.com/cs-cz/library...

Váš případ mě zaujal, protože se tím i já sám něčemu přiučím, něco nového zase poznám, i když to teď zrovna nepotřebuji. Avšak mám tu zkušenost, že když jsem si to dneska vyzkoušel, zítra na to přijde projekt a aplikační využití :).

https://www.codeproject.com/KB/GDI-plus/

A já si tady zatím jen tak prošel práci nad bitmapou, práci s pseudo pointerem ve VB, strukturu a konverzi barev Bitmapy (.Stride), na codeprojectu je dalších x zajímavých věcí v sekci GDI+ (krom svlékání podprsenek i filtrování barev, jak se počítá gamma, světlost, kontrast, počítačové vidění - detekce v obrazu, detekce hran, vytváření vodoznaku, generování code 128 - to jsem už znal :) a tak dále...

Jako člověk se přiučí :) a to mě na tom baví. Dám tomu dva dny a dalších několik desítek ušetřím příště na projektu.

Ted ještě koukám na Bitmap.MakeTransparent. https://msdn.microsoft.com/en-us/library...

Nedalo by se to nějak využít? Jak je to rychlé?

Případně jsem našel tohle, to je taky zajímavé - ale dost podoboné tomu Vašemu.

http://stackoverflow.com/questions/48202...

nebo

http://stackoverflow.com/questions/24814...

Dají se už jen prostě dělat optimalizace toho samotného hledání souřadnic (např. dřívější opouštění cyklů -> jistá část dat obrzáku se nemusí již testovat, pokud je již nějaká veličina dostatečně jasně stanovená)

nahlásit spamnahlásit spam 0 odpovědětodpovědět

Inspiroval jsem dle Fast Pixel Operations in .NET (alias pink bra)

https://www.codeproject.com/articles/617...

a podle polského autora na doporučení druhého nadšence přes podprdy ve vlákně :) rozdělil plnění

bodů do List(of Point) do čtyř úloh Task. Naprosto skvělé...

Potýkal jsem se s chybou přetečení kapacity listu, kterou jsem nakonec nastavil před vytvořením

paraleních úloh na maximální počet možných bodů v obrázku.

Pro informaci výsledky na třech různých obrázcích :

1)Original size: 640 x 480 Points: 49785 / 25 ms Final size: 519 x 387

2)Original size: 640 x 480 Points: 140285 / 33 ms Final size: 342 x 479

3)Original size: 640 x 480 Points: 57053 / 21 ms Final size: 273 x 323

nahlásit spamnahlásit spam 0 odpovědětodpovědět
                       
Nadpis:
Antispam: Komu se občas házejí perly?
Příspěvek bude publikován pod identitou   anonym.
  • Administrátoři si vyhrazují právo komentáře upravovat či mazat bez udání důvodu.
    Mazány budou zejména komentáře obsahující vulgarity nebo porušující pravidla publikování.
  • Pokud nejste zaregistrováni, Vaše IP adresa bude zveřejněna. Pokud s tímto nesouhlasíte, příspěvek neodesílejte.

přihlásit pomocí externího účtu

přihlásit pomocí jména a hesla

Uživatel:  
Heslo:  

zapomenuté heslo

 

založit nový uživatelský účet

zaregistrujte se

 
zavřít

Nahlásit spam

Opravdu chcete tento příspěvek nahlásit pro porušování pravidel fóra?

Nahlásit Zrušit

Chyba

zavřít

feedback