Pixelsearch - count all in an area

Get help with using AutoHotkey (v1.1 and older) and its commands and hotkeys
Boxgoat
Posts: 4
Joined: 14 Oct 2020, 10:42

Pixelsearch - count all in an area

14 Oct 2020, 11:21

Hi folks,

I'm new to AHK and to programming in general. I've been puzzling over writing this for the last 3 days (don't laugh) and whilst it now works I suspect I've massively overcomplicated the whole thing and would appreciate a critique. I also don't fully understand what I've written or some of the problems I encountered..

The premise is that the script will search a defined area for (colour) and count the number of pixels found. I've made something similar work using PixelGetColor but it was taking too long to return a result (multiple seconds for the same search area).

Code: Select all

CoordMode, Pixel, Relative
PixelFindandCount:

count := 0
; Search area is a small rectangle in MS Paint with red pixels marked 
x1 := 13
y1 := 150
x2 := 24
y2 := 164
OuterloopErr := 0

Loop
{
	PixelSearch, cx, cy, x1, y1, x2, y2, 0xED1C24, 1, Fast RGB
		If Errorlevel = 1
			OuterloopErr := 1
		If ErrorLevel = 0
		{
			count++
			px1 := (cx + 1) 
			px2 := x2
			py1 := cy
			py2 := cy
				Loop ; this loop is to complete counting on (y) where pixel found.   
					{
					PixelSearch, cx2, cy2, px1, py1, px2, py2, 0xED1C24, 1, Fast RGB
					If Errorlevel = 0
						{
						count++
						px1 := (cx2 + 1) ; If colour found then increment x
						}
					if Errorlevel = 1 
						{
						y1 := (cy + 1) ; if not found then increment y and break loop
						break
						}
					}
			continue
		}
		If OuterloopErr = 1 ; Have used a variable here as Errorlevel seemed to be pulling through from the inner loop? 
			Msgbox, Search complete. %count% found
			break
}
Any help or criticism (constructive ideally) much appreciated.
Cheers
User avatar
boiler
Posts: 16931
Joined: 21 Dec 2014, 02:44

Re: Pixelsearch - count all in an area

14 Oct 2020, 12:11

You may consider this a little more straightforward using a user-defined function:

Code: Select all

CoordMode, Pixel, Relative
count := PixelCount(0xED1C24, 13, 150, 24, 164, 1)
MsgBox, Search complete. %count% found
return

PixelCount(colorID, x1 := 0, y1 := 0, x2 := "Screen", y2 := "Screen", var := 0, mode := "Fast RGB")
{
	x2 := x2 = "Screen" ? A_ScreenWidth : x2
	y2 := y2 = "Screen" ? A_ScreenHeight : y2
	count := 0
	y := y1
	loop {
		x := x1
		loop {
			; ToolTip, % x "," y ; uncomment to see progress
			PixelSearch, foundX, foundY, x, y, x2, y, colorID, var, % mode
			if (ErrorLevel = 2)
				return -1
			if !ErrorLevel {
				count++
				x := foundX + 1
				lastFoundY := foundY
				if (x > x2)
					break
			}
		} until ErrorLevel
		y++
	} until y > y2
	return count
}

In case you're not really familiar with functions, you don't repeat the function code every time you want to use it, you just call it again with whatever parameters you want, such as:

Code: Select all

count2 := PixelCount(0xFF0000, 0, 150, 200, 500)
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: Pixelsearch - count all in an area

14 Oct 2020, 14:42

If You want speed do not use PixelSearch and pixelgetcolor for this task.
Use GDI+.
Boxgoat
Posts: 4
Joined: 14 Oct 2020, 10:42

Re: Pixelsearch - count all in an area

14 Oct 2020, 15:07

Thanks both. I've seen GDI referenced elsewhere so will look into that.

I'm still dissecting your code boiler, I think I get most of it but could you explain this line for me?

Code: Select all

x2 := x2 = "Screen" ? A_ScreenWidth : x2
User avatar
boiler
Posts: 16931
Joined: 21 Dec 2014, 02:44

Re: Pixelsearch - count all in an area

14 Oct 2020, 16:41

Boxgoat wrote:
14 Oct 2020, 15:07
could you explain this line for me?

Code: Select all

x2 := x2 = "Screen" ? A_ScreenWidth : x2
The right side of the assignment statement is a ternary operation which is a shorthand for if-else (see the ternary operator described in the documentation for Operators in Expressions). The result of which is assigned to x2. So depending on the value of x2, it will assign x2 either the value in the variable A_ScreenWidth or its own original value. It is the equivalent of the following code:

Code: Select all

if (x2 = "Screen")
	x2 := A_ScreenWdith
else
	x2 := x2
The reason for this is so the user of the function can pass the string "Screen" and the function will then assign the value of the screen width (A_ScreenWidth) to x2, and if it's not that string, then just leave it with the value it already had (by assigning its value to itself).
User avatar
boiler
Posts: 16931
Joined: 21 Dec 2014, 02:44

Re: Pixelsearch - count all in an area

14 Oct 2020, 18:14

Here's how you can do it using GDI+. If your Gdip file is named differently and/or not in your using library folder, then replace <gdip_all> with the path to your Gdip file. The other #Include assumes that you have the function shown in the second script in a file named Gdip_PixelCount.ahk, but change that as necessary as well to point to your path for it (or just put the function in the same file as your main script).

Your main script:

Code: Select all

#Include <gdip_all>
#Include Gdip_PixelCount.ahk

F1:: ; press F1 key when window of interest is active
	WinGetPos, wX, wY,,, A ; get offsets for active window so you can pass screen coords
	count := Gdip_PixelCount(13 + wX, 150 + wY, 24 + wX, 164 + wY, 0xED1C24, 1)
	MsgBox, Search complete. %count% found
return

A separate script named Gdip_PixelCount:

Code: Select all

Gdip_PixelCount(x1, y1, x2, y2, color, var:=0) {
	count := 0
	color := 0xFF000000 | color ; add alpha channel
	pToken := Gdip_Startup()
	targetR := Gdip_RFromARGB(color)
	targetG := Gdip_GFromARGB(color)
	targetB := Gdip_BFromARGB(color)
	pBitmap := Gdip_BitmapFromScreen(x1 . "|" . y1 . "|" (x2 - x1 + 1) . "|" (y2 - y1 + 1)) 
	Gdip_GetImageDimensions(pBitmap, w, h)
	Gdip_LockBits(pBitmap, 0, 0, w, h, stride, scan, bitmapData)
	loop, % h {
		y := A_Index - 1
		loop, % w {
			x := A_Index - 1
			thisColor := Gdip_GetLockBitPixel(scan, x, y, stride)
			thisR := Gdip_RFromARGB(thisColor)
			thisG := Gdip_GFromARGB(thisColor)
			thisB := Gdip_BFromARGB(thisColor)
			if (Abs(thisR - targetR) <= var) && (Abs(thisG - targetG) <= var) && (Abs(thisB - targetB) <= var)
				count++
		}
	}
	Gdip_UnlockBits(pBitmap, bitmapData)
	Gdip_DisposeImage(pBitmap)
	Gdip_Shutdown(pToken)
	return count
}

It can be made significantly faster by writing an MCode function to do the actual color comparisons, which I might do later.
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: Pixelsearch - count all in an area

14 Oct 2020, 18:49

It can be more than 4 times faster without machine code.
Just dont use functions.

Code: Select all

loop, % h
{
   y := A_Index - 1
   loop, % w
   {
      if (Abs(((0x00ff0000 & NumGet(Scan+0, (A_Index-1)*4 + y*Stride, "uint")) >> 16) - targetR) <= var) && (Abs(((0x0000ff00 & NumGet(Scan+0, (A_Index-1)*4 + y*Stride, "uint")) >> 8) - targetG) <= var) && (Abs((0x000000ff & NumGet(Scan+0, (A_Index-1)*4 + y*Stride, "uint")) - targetB) <= var)
         count++
   }
}
User avatar
boiler
Posts: 16931
Joined: 21 Dec 2014, 02:44

Re: Pixelsearch - count all in an area

14 Oct 2020, 22:30

How do you figure more than 4 times faster? I'm seeing a few percent. For the size of search area I used (the one in the OP), the difference between any two trials of the same approach can be many times larger (like 20 or 30 ms) than the average difference between the two approaches (about 2 ms).
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: Pixelsearch - count all in an area

15 Oct 2020, 00:01

Code: Select all

setbatchlines -1
loop 5
   Gdip_PixelCount(0, 0, 1000, 1000, 0xED1C24, 1)





Gdip_PixelCount(x1, y1, x2, y2, color, var:=0) {
	count := 0
	color := 0xFF000000 | color ; add alpha channel
	pToken := Gdip_Startup()
	targetR := Gdip_RFromARGB(color)
	targetG := Gdip_GFromARGB(color)
	targetB := Gdip_BFromARGB(color)
	pBitmap := Gdip_BitmapFromScreen(x1 . "|" . y1 . "|" (x2 - x1 + 1) . "|" (y2 - y1 + 1)) 
	Gdip_GetImageDimensions(pBitmap, w, h)
	Gdip_LockBits(pBitmap, 0, 0, w, h, stride, scan, bitmapData)

a := A_tickcount
	loop, % h {
		y := A_Index - 1
		loop, % w {
			x := A_Index - 1
			thisColor := Gdip_GetLockBitPixel(scan, x, y, stride)
			thisR := Gdip_RFromARGB(thisColor)
			thisG := Gdip_GFromARGB(thisColor)
			thisB := Gdip_BFromARGB(thisColor)
			if (Abs(thisR - targetR) <= var) && (Abs(thisG - targetG) <= var) && (Abs(thisB - targetB) <= var)
				count++
		}
	}
a1 := A_Tickcount - a
b := A_tickcount
loop, % h
{
   y := A_Index - 1
   loop, % w
   {
      if (Abs(((0x00ff0000 & NumGet(Scan+0, (A_Index-1)*4 + y*Stride, "uint")) >> 16) - targetR) <= var) && (Abs(((0x0000ff00 & NumGet(Scan+0, (A_Index-1)*4 + y*Stride, "uint")) >> 8) - targetG) <= var) && (Abs((0x000000ff & NumGet(Scan+0, (A_Index-1)*4 + y*Stride, "uint")) - targetB) <= var)
         count++
   }
}
b1 := A_Tickcount - b
msgbox % a1 "`n" b1

	Gdip_UnlockBits(pBitmap, bitmapData)
	Gdip_DisposeImage(pBitmap)
	Gdip_Shutdown(pToken)
	return count
}
malcev
Posts: 1769
Joined: 12 Aug 2014, 12:37

Re: Pixelsearch - count all in an area

28 Oct 2020, 16:10

BTW, using gdi or gdi+ approach to get scan the fastest method is to use CreateDIBSection than GetDIBits or lockbits.

Code: Select all

setbatchlines -1
DllCall("LoadLibrary", "str", "gdiplus")
VarSetCapacity(si, 8+2*A_PtrSize, 0)
NumPut(0x1, si, "uint")
DllCall("gdiplus\GdiplusStartup", "ptr*", pToken, "ptr", &si, "ptr", 0)
hDC := DllCall("GetDC", "ptr", 0, "ptr")
pDC := DllCall("CreateCompatibleDC", "ptr", hDC, "ptr")
VarSetCapacity(bi, 40, 0)
NumPut(40, bi, 0, "int")
NumPut(A_ScreenWidth, bi, 4, "int")
NumPut(A_ScreenHeight, bi, 8, "int")
NumPut(1, bi, 12, "short")
NumPut(bpp:=32, bi, 14, "short")
hBM := DllCall("CreateDIBSection", "ptr", pDC, "ptr", &bi, "int", 0, "ptr*", Scan, "int", 0, "int", 0, "ptr")
Stride:=((A_ScreenWidth*bpp+31)//32)*4
oBM := DllCall("SelectObject", "ptr", pDC, "ptr", hBM, "ptr")

a := a_tickcount
loop 100
   DllCall("BitBlt", "ptr", pDC, "int", 0, "int", 0, "int", A_ScreenWidth, "int", A_ScreenHeight, "ptr", hDC, "int", 0, "int", 0, "uint", 0x00CC0020|0x40000000) ; SourceCopy | CaptureBlt
msgbox % a_tickcount - a


hBM := DllCall("CreateCompatibleBitmap", "ptr", hDC, "int", A_ScreenWidth, "int", A_ScreenHeight, "ptr")
oBM := DllCall("SelectObject", "ptr", pDC, "ptr", hBM, "ptr")
VarSetCapacity(bi, 40, 0)
NumPut(40, bi, 0, "int")
NumPut(A_ScreenWidth, bi, 4, "int")
NumPut(-A_ScreenHeight, bi, 8, "int")
NumPut(1, bi, 12, "short")
NumPut(bpp:=32, bi, 14, "short")
Stride:=((A_ScreenWidth*bpp+31)//32)*4

a := a_tickcount
loop 100
{
   VarSetCapacity(bits, A_ScreenWidth*A_ScreenHeight*4, 0)
   DllCall("BitBlt", "ptr", pDC, "int", 0, "int", 0, "int", A_ScreenWidth, "int", A_ScreenHeight, "ptr", hDC, "int", 0, "int", 0, "uint", 0x00CC0020|0x40000000) ; SourceCopy | CaptureBlt
   DllCall("GetDIBits", "ptr", pDC, "ptr", hBM, "int", 0, "int", A_ScreenHeight, "ptr", &bits, "ptr", &bi, "int", 0)
   Scan := &bits
}
msgbox % a_tickcount - a


hBM := DllCall("CreateCompatibleBitmap", "ptr", hDC, "int", A_ScreenWidth, "int", A_ScreenHeight, "ptr")
oBM := DllCall("SelectObject", "ptr", pDC, "ptr", hBM, "ptr")
VarSetCapacity(Rect, 16, 0)
NumPut(A_ScreenWidth, Rect, 8, "uint")
NumPut(A_ScreenHeight, Rect, 12, "uint")

a := a_tickcount
loop 100
{
   DllCall("BitBlt", "ptr", pDC, "int", 0, "int", 0, "int", A_ScreenWidth, "int", A_ScreenHeight, "ptr", hDC, "int", 0, "int", 0, "uint", 0x00CC0020|0x40000000) ; SourceCopy | CaptureBlt
   DllCall("gdiplus\GdipCreateBitmapFromHBITMAP", "ptr", hBM, "ptr", 0, "ptr*", pBitmap)
   VarSetCapacity(BitmapData, 16+2*A_PtrSize, 0)
   DllCall("gdiplus\GdipBitmapLockBits", "ptr", pBitmap, "ptr", &Rect, "uint", ReadWrite := 3, "int", Format32bppArgb := 2498570, "ptr", &BitmapData)
   Stride := NumGet(BitmapData, 8, "int")
   Scan := NumGet(BitmapData, 16, "ptr")
   DllCall("gdiplus\GdipBitmapUnlockBits", "ptr", pBitmap, "ptr", &BitmapData)
   DllCall("gdiplus\GdipDisposeImage", "ptr", pBitmap)
}
msgbox % a_tickcount - a

Return to “Ask for Help (v1)”

Who is online

Users browsing this forum: scriptor2016 and 266 guests