Fast ScanRegion (now very fast)

Share your advanced PureBasic knowledge/code with the community.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Fast ScanRegion (now very fast)

Post by netmaestro »

Note: ** Latest version found here: http://www.purebasic.fr/english/viewtop ... 0&start=24

There are several versions of a ScanRegion procedure to be found on the internet in a variety of languages, but they all (at least the ones I can find) use pretty much the same method, traversing through the bitmap with a pointer and excluding transparent pixels from the region as it finds them. This works well, but it's very slow. The image I'm providing with this version,

http://www.lloydsplace.com/girlonchair.bmp

using the straight StartDrawing() and Point() method, took over 3000 ms to complete. Switching to a structured pointer traversing a BITMAP object speeded it up a whole 500 ms, leaving 2500+ milliseconds it was taking to scan the region. I wanted to use the SetWindowRgn API to create a transparent imagegadget, but at these speeds it's just unworkable. Some tests showed that the bottleneck was located in the large number of CombineRgn calls, these things are slow. So I realized that most transparent pixels have another transparent pixel beside them, usually quite a few. What if we just save them up until there's no more in this group and do one CombineRgn with all of them? So I came up with the following procedure. It brought the time taken to scan and set the window region from 2500+ ms down to 78 ms on my machine. At this level of performance, a transparent imagegadget is possible using this method. I haven't seen any scan procs this fast anywhere, so I'm sharing it:

Code: Select all

Procedure ScanRegion(ImageID, transcolor)

  Structure RGBTRIPLEC
    rgbtBlue.c
    rgbtGreen.c
    rgbtRed.c
  EndStructure

  GetObject_(ImageID, SizeOf(BITMAP), @bmp.BITMAP)
  Protected width      = bmp\bmWidth
  Protected height     = bmp\bmHeight
  Protected hRgn       = CreateRectRgn_(0, 0, width, height)
  Protected tred       = Red(transcolor)
  Protected tgreen     = Green(transcolor)
  Protected tblue      = Blue(transcolor)
 
  BmiInfo.BITMAPINFOHEADER
  With BmiInfo 
    \biSize         = SizeOf(BITMAPINFOHEADER) 
    \biWidth        = width 
    \biHeight       = -height 
    \biPlanes       = 1 
    \biBitCount     = 24 
    \biCompression  = #BI_RGB
  EndWith   
 
  bytesperrow = 4*((3*width+3)/4) 

  *bits = AllocateMemory(bytesperrow*height) 
  hDC = GetWindowDC_(#Null) 
  iRes = GetDIBits_(hDC, ImageID, 0, height, *bits, @bmiInfo, #DIB_RGB_COLORS) 
  ReleaseDC_(#Null, hDC)  
  
  For y=0 To height-1
    pxcount=0
    For x=0 To bytesperrow-1 Step 3
      *px.RGBTRIPLEC = *bits + bytesperrow * y + x
      If *px\rgbtRed=tred And *px\rgbtGreen=tgreen And *px\rgbtBlue=tblue
        transcount = 1  
        firsttrans = pxcount 
        While *px\rgbtRed=tred And *px\rgbtGreen=tgreen And *px\rgbtBlue=tblue And x <= bytesperrow-4 
          transcount+1 
          x+3     
          pxcount+1    
          *px = *bits + bytesperrow * y + x 
        Wend
        hTmpRgn = CreateRectRgn_(firsttrans,y,firsttrans+transcount,y+1)
        CombineRgn_(hRgn, hRgn, hTmpRgn, #RGN_XOR)
        DeleteObject_(hTmpRgn)
      EndIf
      pxcount+1
    Next
  Next
  FreeMemory(*bits)
  ProcedureReturn hRgn
EndProcedure

LoadImage(0, "girl.bmp")

OpenWindow(0,0,0,ImageWidth(0),ImageHeight(0),"", #PB_Window_ScreenCentered|#PB_Window_BorderLess)
CreateGadgetList(WindowID(0))
ImageGadget(0,0,0,0,0,ImageID(0))
start = ElapsedMilliseconds()
SetWindowRgn_(WindowID(0), ScanRegion(ImageID(image), #White), #True)
ending = ElapsedMilliseconds()
MessageRequester("Created Region in:", Str(ending-start)+" milliseconds",$C0)
Repeat
  WaitWindowEvent()
Until GetAsyncKeyState_(#VK_SPACE) & 32768
The spacebar closes the window, speed test without debugger.

Can you speed it up more yet? If so, please post the code. Or, if you know where a faster one already is, if you could post a link it would be appreciated. Thanks!
Last edited by netmaestro on Mon Jan 03, 2011 7:10 am, edited 6 times in total.
BERESHEIT
User avatar
Mischa
Enthusiast
Enthusiast
Posts: 115
Joined: Fri Aug 15, 2003 7:43 pm

Post by Mischa »

Hi!

I have done this before. :wink:

http://www.purebasic.fr/german/viewtopic.php?t=6126
Could be faster, you're rigt.

There's a tip by hroudtwolf inside this topic to use a faster method.
But this one will work not with 95,98,ME.

Check it out.

Regards
thefool
Always Here
Always Here
Posts: 5875
Joined: Sat Aug 30, 2003 5:58 pm
Location: Denmark

Post by thefool »

Hroutwolf's example has nothing to do with this. The whole point of this is his idea on how to analyze the image faster!

And this is source. Your work is a closed library that does not work with pb 4.1!


Nice code & ideas, netmaestro :)
User avatar
Mischa
Enthusiast
Enthusiast
Posts: 115
Joined: Fri Aug 15, 2003 7:43 pm

Post by Mischa »

Sorry, i thought the source is included. :oops:
Now, it IS included.

Can you speed it up more yet? If so, please post the code. Or, if you know where a faster one already is, if you could post a link it would be appreciated. Thanks!
If you want to set window region, SetLayeredWindowAttributes_()
is the fastest way i know. (OS > Me)

The other way is to save calculated regions and load it later.
This can be simple done the API-way.


Ok, there exist another method.
You can calculate an outline mask.

http://www.purebasic.fr/english/viewtopic.php?t=2283

But this one works only with bitmap-sources without holes.
This one should be very fast (when you optimize it a bit)


Regards
thefool
Always Here
Always Here
Posts: 5875
Joined: Sat Aug 30, 2003 5:58 pm
Location: Denmark

Post by thefool »

Mischa wrote:Sorry, i thought the source is included. :oops:
Now, it IS included.
thanks :)
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

It's not about window skinning at all, as I want to create a Transparent ImageGadget control. With window skins you can calculate and save the region data ahead of time, so speed isn't a factor. For the gadget idea, a new region has to be created on the fly in real time whenever a SetGadgetState happens. For this to be at all workable in practice, a quick region creation is vital. I'm getting pretty close, actually what I have now is only taking 78 ms here for a 300 x 400 image that's full of little holes.
BERESHEIT
Dare
Addict
Addict
Posts: 1965
Joined: Mon May 29, 2006 1:01 am
Location: Outback

Post by Dare »

That is blooming awesome, netmaestro. Thanks.
Dare2 cut down to size
eesau
Enthusiast
Enthusiast
Posts: 589
Joined: Fri Apr 27, 2007 12:38 pm
Location: Finland

Post by eesau »

A faster way is to abandon CombineRgn and use ExtCreateRegion instead. Get the bitmap bits as before using GetDIBits (using biBitCount of 32 instead of 24 would be better though so you could do only one dword comparison instead comparing three bytes), then go through the bitmap data and create RECTs as you go, storing them. When finished, call ExtCreateRegion with the RECT data, and you should have your region.

Setting up ExtCreateRegion is a bit involved though, but should be faster than lots of CombineRgn calls. I have some old C-code that does this, I'll try to port it later.

Here's to get you started:

MSDN: ExtCreateRegion
Window shapes in VB (with code)
AsmCommunity forum thread (with code)
eesau
Enthusiast
Enthusiast
Posts: 589
Joined: Fri Apr 27, 2007 12:38 pm
Location: Finland

Post by eesau »

Forgot to mention something important:

On Win95/98/NT ExtCreateRegion fails if the RECT data you pass to it holds more than 4096 rectangles. This really shouldn't be a problem with small to medium sized images, but you can still work around this by creating several regions, each using less than 4096 rectangles, and then combining those with CombineRgn. The function also fails on the aforementioned platforms if the data you pass to it contains overlapped rectangles or transformation data.

More about that here and here.
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

eesau, excellent excellent tip! :D :D :D

I can't thank you enough for your idea. Incorporating your proposal with what I already have cut the 78 milliseconds down to 5 milliseconds for the same image. I had to switch to a hi-res timing method just to measure it. Here's the new and much faster procedure, with testing code:

Code: Select all

ProcedureDLL.l TicksHQ() 
  Static maxfreq.q 
  Protected t.q 
  If maxfreq=0 
    QueryPerformanceFrequency_(@maxfreq) 
    maxfreq=maxfreq/1000
  EndIf 
  QueryPerformanceCounter_(@t) 
  ProcedureReturn t/maxfreq 
EndProcedure 


Structure REGIONDATA
  rdh.RGNDATAHEADER
  buffer.l[100000]
EndStructure

Procedure ScanRegion(ImageID, transcolor)

  ;======================================================= 
  ;                                                      = 
  ;      Very fast bitmap -> region creator              = 
  ;                                                      = 
  ;      By netmaestro with creative input from eesau    = 
  ;                                                      = 
  ;      June 26, 2007                                   = 
  ;                                                      = 
  ;======================================================= 

  Structure RGBTRIPLEC
    rgbtBlue.c
    rgbtGreen.c
    rgbtRed.c
  EndStructure

  GetObject_(ImageID, SizeOf(BITMAP), @bmp.BITMAP)
  Protected width       = bmp\bmWidth
  Protected height      = bmp\bmHeight
  Protected hVisibleRgn = CreateRectRgn_(0, 0, width, height)
  Protected tred        = Red(transcolor)
  Protected tgreen      = Green(transcolor)
  Protected tblue       = Blue(transcolor)
 
  BmiInfo.BITMAPINFOHEADER
  With BmiInfo 
    \biSize         = SizeOf(BITMAPINFOHEADER) 
    \biWidth        = width 
    \biHeight       = -height 
    \biPlanes       = 1 
    \biBitCount     = 24 
    \biCompression  = #BI_RGB
  EndWith   
 
  bytesperrow = 4*((3*width+3)/4) 

  *bits = AllocateMemory(bytesperrow*height) 
  hDC   = GetWindowDC_(#Null) 
  iRes  = GetDIBits_(hDC, ImageID, 0, height, *bits, @bmiInfo, #DIB_RGB_COLORS) 
  ReleaseDC_(#Null, hDC)  
  
  Shared rd.REGIONDATA
 
  bufferloc = 0 : rectcount = 0
  For y=0 To height-1
    pxcount=0
    For x=0 To bytesperrow-1 Step 3
      *px.RGBTRIPLEC = *bits + bytesperrow * y + x
      If *px\rgbtRed=tred And *px\rgbtGreen=tgreen And *px\rgbtBlue=tblue
        transcount = 1 : firsttrans = pxcount 
        While *px\rgbtRed=tred And *px\rgbtGreen=tgreen And *px\rgbtBlue=tblue And x <= bytesperrow-4 
          transcount+1 : pxcount+1 : x+3     
          *px = *bits + bytesperrow * y + x 
        Wend
        rectcount+1
        rd\buffer[bufferloc] = firsttrans            : bufferloc+1
        rd\buffer[bufferloc] = y                     : bufferloc+1
        rd\buffer[bufferloc] = firsttrans+transcount : bufferloc+1
        rd\buffer[bufferloc] = y+1                   : bufferloc+1
      EndIf
      pxcount+1
    Next
  Next
  FreeMemory(*bits)
  
  With rd\rdh
    \dwSize         = SizeOf(RGNDATAHEADER)
    \iType          = #RDH_RECTANGLES 
    \nCount         = rectcount
    \nRgnSize       = rectcount * SizeOf(RECT)
    \rcBound\left   = 0
    \rcBound\top    = 0
    \rcBound\right  = width
    \rcBound\bottom = height
  EndWith  
  
  hTransparentRgn = ExtCreateRegion_(#Null, SizeOf(rd), @rd)
  CombineRgn_(hVisibleRgn, hVisibleRgn, hTransparentRgn, #RGN_XOR)
  DeleteObject_(hTransparentRgn)
  RtlZeroMemory_(@rd\buffer, 100000)
  
  ProcedureReturn hVisibleRgn
  
EndProcedure

LoadImage(0, "girl.bmp") 

OpenWindow(0,0,0,ImageWidth(0),ImageHeight(0),"", #PB_Window_ScreenCentered|#PB_Window_BorderLess|#PB_Window_Invisible) 
CreateGadgetList(WindowID(0)) 
ImageGadget(0,0,0,0,0,ImageID(0)) 

start = TicksHQ() 
  region = ScanRegion(ImageID(0), #White)
ending = TicksHQ() 

SetWindowRgn_(WindowID(0), region, #True) 
HideWindow(0,0)
MessageRequester("Created Region in:", Str(ending-start)+" milliseconds",$C0) 
Repeat 
  WaitWindowEvent() 
Until GetAsyncKeyState_(#VK_SPACE) & 32768 
Lots fast enough for a transparent image gadget.

If you're speed testing, remember, no debugger.

I'd bet that nobody anywhere is creating regions from bitmaps faster than this :!: hehe
Last edited by netmaestro on Wed Jun 27, 2007 5:54 am, edited 1 time in total.
BERESHEIT
eesau
Enthusiast
Enthusiast
Posts: 589
Joined: Fri Apr 27, 2007 12:38 pm
Location: Finland

Post by eesau »

netmaestro wrote:eesau, excellent excellent tip! :D :D :D

I'd bet that nobody anywhere is creating regions from bitmaps faster than this :!: hehe
You're welcome :)

Excellent code, by the way! And very, very useful.
Dare
Addict
Addict
Posts: 1965
Joined: Mon May 29, 2006 1:01 am
Location: Outback

Post by Dare »

Wow.

That is very fast indeed!
Dare2 cut down to size
User avatar
IceSoft
Addict
Addict
Posts: 1695
Joined: Thu Jun 24, 2004 8:51 am
Location: Germany

Post by IceSoft »

Can I use an OpenWindowedScreen() on the example too?
Belive! C++ version of Puzzle of Mystralia
Bug Planet
<Wrapper>4PB, PB<game>, =QONK=, PetriDish, Movie2Image, PictureManager,...
User avatar
netmaestro
PureBasic Bullfrog
PureBasic Bullfrog
Posts: 8451
Joined: Wed Jul 06, 2005 5:42 am
Location: Fort Nelson, BC, Canada

Post by netmaestro »

Sure, why not? The SetWindowRgn shouldn't interfere with a windowedscreen at all afaik. Go for it :wink:
BERESHEIT
Nico
Enthusiast
Enthusiast
Posts: 274
Joined: Sun Jan 11, 2004 11:34 am
Location: France

Post by Nico »

Code: Select all

; I made some modification to remove buffer.l[....] in the Structure REGIONDATA
; and allocate dynamically the buffer according to the image 

ProcedureDLL.l TicksHQ()
  Static maxfreq.q
  Protected t.q
  If maxfreq=0
    QueryPerformanceFrequency_(@maxfreq)
    maxfreq=maxfreq/1000
  EndIf
  QueryPerformanceCounter_(@t)
  ProcedureReturn t/maxfreq
EndProcedure

;--------------------------------------------------------
;---------------------Modification-----------------------
Structure REGIONDATA
  rdh.RGNDATAHEADER
EndStructure
;--------------------------------------------------------

Procedure ScanRegion(ImageID, transcolor)

  ;=======================================================
  ;                                                      =
  ;      Very fast bitmap -> region creator              =
  ;                                                      =
  ;      By netmaestro with creative input from eesau    =
  ;                                                      =
  ;      June 26, 2007                                   =
  ;                                                      =
  ;=======================================================

  Structure RGBTRIPLEC
    rgbtBlue.c
    rgbtGreen.c
    rgbtRed.c
  EndStructure

  GetObject_(ImageID, SizeOf(BITMAP), @bmp.BITMAP)
  Protected width       = bmp\bmWidth
  Protected height      = bmp\bmHeight
  Protected hVisibleRgn = CreateRectRgn_(0, 0, width, height)
  Protected tred        = Red(transcolor)
  Protected tgreen      = Green(transcolor)
  Protected tblue       = Blue(transcolor)
 
  BmiInfo.BITMAPINFOHEADER
  With BmiInfo
    \biSize         = SizeOf(BITMAPINFOHEADER)
    \biWidth        = width
    \biHeight       = -height
    \biPlanes       = 1
    \biBitCount     = 24
    \biCompression  = #BI_RGB
  EndWith   
 
  bytesperrow = 4*((3*width+3)/4)

  *bits = AllocateMemory(bytesperrow*height)
  hDC   = GetWindowDC_(#Null)
  iRes  = GetDIBits_(hDC, ImageID, 0, height, *bits, @bmiInfo, #DIB_RGB_COLORS)
  ReleaseDC_(#Null, hDC) 
 
;--------------------------------------------------------
;---------------------Modification-----------------------
  Structure_Maxi=(width*height*16)+SizeOf(REGIONDATA)
  *Buffer.REGIONDATA=AllocateMemory(Structure_Maxi)
  *rd.LONG=*Buffer+SizeOf(REGIONDATA)
;--------------------------------------------------------

  bufferloc = 0 : rectcount = 0
  For y=0 To height-1
    pxcount=0
    For x=0 To bytesperrow-1 Step 3
      *px.RGBTRIPLEC = *bits + bytesperrow * y + x
      If *px\rgbtRed=tred And *px\rgbtGreen=tgreen And *px\rgbtBlue=tblue
        transcount = 1 : firsttrans = pxcount
        While *px\rgbtRed=tred And *px\rgbtGreen=tgreen And *px\rgbtBlue=tblue And x <= bytesperrow-4
          transcount+1 : pxcount+1 : x+3     
          *px = *bits + bytesperrow * y + x
        Wend
        rectcount+1
;--------------------------------------------------------
;---------------------Modification-----------------------
        *rd\l = firsttrans            : *rd+4
        *rd\l = y                     : *rd+4
        *rd\l = firsttrans+transcount : *rd+4
        *rd\l = y+1                   : *rd+4
;--------------------------------------------------------
      EndIf
      pxcount+1
    Next
  Next
  FreeMemory(*bits)
 
 
  With *Buffer\rdh
    \dwSize         = SizeOf(RGNDATAHEADER)
    \iType          = #RDH_RECTANGLES
    \nCount         = rectcount
    \nRgnSize       = rectcount * SizeOf(RECT)
    \rcBound\left   = 0
    \rcBound\top    = 0
    \rcBound\right  = width
    \rcBound\bottom = height
  EndWith 
  
;--------------------------------------------------------
;---------------------Modification-----------------------
  taille=SizeOf(RGNDATAHEADER)+(rectcount * SizeOf(RECT))
  hTransparentRgn = ExtCreateRegion_(#Null, taille, *Buffer)
  FreeMemory(*Buffer)
;--------------------------------------------------------

  CombineRgn_(hVisibleRgn, hVisibleRgn, hTransparentRgn, #RGN_XOR)
  DeleteObject_(hTransparentRgn)
  

 
  ProcedureReturn hVisibleRgn
 
EndProcedure

LoadImage(0, "c:\girl.bmp")

OpenWindow(0,0,0,ImageWidth(0),ImageHeight(0),"", #PB_Window_ScreenCentered|#PB_Window_BorderLess|#PB_Window_Invisible)
CreateGadgetList(WindowID(0))
ImageGadget(0,0,0,0,0,ImageID(0))

start = TicksHQ()
  region = ScanRegion(ImageID(0), #White)
ending = TicksHQ()

SetWindowRgn_(WindowID(0), region, #True)
HideWindow(0,0)
MessageRequester("Created Region in:", Str(ending-start)+" milliseconds",$C0)
Repeat
  WaitWindowEvent()
Until GetAsyncKeyState_(#VK_SPACE) & 32768
Post Reply