detecting tile squares in 2048 with cv2












1















I am learning cv2 and trying to detect the on-board number tiles (dynamically) from the 2048 game, and outlining them in green.



Firstly I am having trouble detecting the ones in the more orange to red range (8, 16, 32, 64) and if I lower the threshold the whole board seems to be included. Sometimes, smaller parts (such as the round part of a 6 are included) or an entire tile is ignored. How would I go about detecting the tiles on a board like this?



Here is the code I have so far:



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))
resized_img = cv2.resize(img, (100, 100))
gray_img = cv2.cvtColor(resized_img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray_img, 200, 255, 0)[1]
contours = cv2.findContours(thresh, 1, 2)[1]
for cnt in contours:
if len(cnt) == 4:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


sample detection:



enter image description here



EDIT: added sample input as requested



sample input:



enter image description here



sample output:



enter image description here



Thankyou for any responses, even if they are a point in the right direction










share|improve this question

























  • It looks like you are having boxes with defined colors at fixed positions. So, there is not much sense in detecting their position and they should be rather easy to identify if you filter for the right color(s).

    – Klaus D.
    Nov 23 '18 at 2:33











  • @KlausD. this was more of an exercise to determine if I could dynamically work out the position of the boxes as practice using cv2 - so if the boxes were layed out randomly, what the method would be to detect them. How would I go about filtering the colours if I were to do it using that method?

    – NightShade
    Nov 23 '18 at 3:14













  • Can you please attach full size sample input image? Do you want to detect the boxes only or do you also want to segment the boxes on the basis of color?

    – ZdaR
    Nov 23 '18 at 4:13











  • You need to filter the image more precisely... Try using some edge detection open CV algo with dilating and eroding. To prevent whole box to be selected or only smaller parts ( round of 6 ) to be selected as contour just check area of every contour.... Opencv has facility to do this. Once you have area of each contour then drop the contour if it is very large or very small. This will filter all unnecessary contours and you will get the desired contours....

    – Devashish Prasad
    Nov 23 '18 at 4:14











  • @ZdaR I have edited the post and provided a large sample input image and sample output made in paint. I only want to detect the individual game tiles, with no concern for the colour or value within them

    – NightShade
    Nov 23 '18 at 4:38
















1















I am learning cv2 and trying to detect the on-board number tiles (dynamically) from the 2048 game, and outlining them in green.



Firstly I am having trouble detecting the ones in the more orange to red range (8, 16, 32, 64) and if I lower the threshold the whole board seems to be included. Sometimes, smaller parts (such as the round part of a 6 are included) or an entire tile is ignored. How would I go about detecting the tiles on a board like this?



Here is the code I have so far:



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))
resized_img = cv2.resize(img, (100, 100))
gray_img = cv2.cvtColor(resized_img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray_img, 200, 255, 0)[1]
contours = cv2.findContours(thresh, 1, 2)[1]
for cnt in contours:
if len(cnt) == 4:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


sample detection:



enter image description here



EDIT: added sample input as requested



sample input:



enter image description here



sample output:



enter image description here



Thankyou for any responses, even if they are a point in the right direction










share|improve this question

























  • It looks like you are having boxes with defined colors at fixed positions. So, there is not much sense in detecting their position and they should be rather easy to identify if you filter for the right color(s).

    – Klaus D.
    Nov 23 '18 at 2:33











  • @KlausD. this was more of an exercise to determine if I could dynamically work out the position of the boxes as practice using cv2 - so if the boxes were layed out randomly, what the method would be to detect them. How would I go about filtering the colours if I were to do it using that method?

    – NightShade
    Nov 23 '18 at 3:14













  • Can you please attach full size sample input image? Do you want to detect the boxes only or do you also want to segment the boxes on the basis of color?

    – ZdaR
    Nov 23 '18 at 4:13











  • You need to filter the image more precisely... Try using some edge detection open CV algo with dilating and eroding. To prevent whole box to be selected or only smaller parts ( round of 6 ) to be selected as contour just check area of every contour.... Opencv has facility to do this. Once you have area of each contour then drop the contour if it is very large or very small. This will filter all unnecessary contours and you will get the desired contours....

    – Devashish Prasad
    Nov 23 '18 at 4:14











  • @ZdaR I have edited the post and provided a large sample input image and sample output made in paint. I only want to detect the individual game tiles, with no concern for the colour or value within them

    – NightShade
    Nov 23 '18 at 4:38














1












1








1








I am learning cv2 and trying to detect the on-board number tiles (dynamically) from the 2048 game, and outlining them in green.



Firstly I am having trouble detecting the ones in the more orange to red range (8, 16, 32, 64) and if I lower the threshold the whole board seems to be included. Sometimes, smaller parts (such as the round part of a 6 are included) or an entire tile is ignored. How would I go about detecting the tiles on a board like this?



Here is the code I have so far:



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))
resized_img = cv2.resize(img, (100, 100))
gray_img = cv2.cvtColor(resized_img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray_img, 200, 255, 0)[1]
contours = cv2.findContours(thresh, 1, 2)[1]
for cnt in contours:
if len(cnt) == 4:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


sample detection:



enter image description here



EDIT: added sample input as requested



sample input:



enter image description here



sample output:



enter image description here



Thankyou for any responses, even if they are a point in the right direction










share|improve this question
















I am learning cv2 and trying to detect the on-board number tiles (dynamically) from the 2048 game, and outlining them in green.



Firstly I am having trouble detecting the ones in the more orange to red range (8, 16, 32, 64) and if I lower the threshold the whole board seems to be included. Sometimes, smaller parts (such as the round part of a 6 are included) or an entire tile is ignored. How would I go about detecting the tiles on a board like this?



Here is the code I have so far:



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))
resized_img = cv2.resize(img, (100, 100))
gray_img = cv2.cvtColor(resized_img, cv2.COLOR_BGR2GRAY)
thresh = cv2.threshold(gray_img, 200, 255, 0)[1]
contours = cv2.findContours(thresh, 1, 2)[1]
for cnt in contours:
if len(cnt) == 4:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


sample detection:



enter image description here



EDIT: added sample input as requested



sample input:



enter image description here



sample output:



enter image description here



Thankyou for any responses, even if they are a point in the right direction







python opencv cv2






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited Nov 23 '18 at 4:36







NightShade

















asked Nov 23 '18 at 2:17









NightShadeNightShade

11410




11410













  • It looks like you are having boxes with defined colors at fixed positions. So, there is not much sense in detecting their position and they should be rather easy to identify if you filter for the right color(s).

    – Klaus D.
    Nov 23 '18 at 2:33











  • @KlausD. this was more of an exercise to determine if I could dynamically work out the position of the boxes as practice using cv2 - so if the boxes were layed out randomly, what the method would be to detect them. How would I go about filtering the colours if I were to do it using that method?

    – NightShade
    Nov 23 '18 at 3:14













  • Can you please attach full size sample input image? Do you want to detect the boxes only or do you also want to segment the boxes on the basis of color?

    – ZdaR
    Nov 23 '18 at 4:13











  • You need to filter the image more precisely... Try using some edge detection open CV algo with dilating and eroding. To prevent whole box to be selected or only smaller parts ( round of 6 ) to be selected as contour just check area of every contour.... Opencv has facility to do this. Once you have area of each contour then drop the contour if it is very large or very small. This will filter all unnecessary contours and you will get the desired contours....

    – Devashish Prasad
    Nov 23 '18 at 4:14











  • @ZdaR I have edited the post and provided a large sample input image and sample output made in paint. I only want to detect the individual game tiles, with no concern for the colour or value within them

    – NightShade
    Nov 23 '18 at 4:38



















  • It looks like you are having boxes with defined colors at fixed positions. So, there is not much sense in detecting their position and they should be rather easy to identify if you filter for the right color(s).

    – Klaus D.
    Nov 23 '18 at 2:33











  • @KlausD. this was more of an exercise to determine if I could dynamically work out the position of the boxes as practice using cv2 - so if the boxes were layed out randomly, what the method would be to detect them. How would I go about filtering the colours if I were to do it using that method?

    – NightShade
    Nov 23 '18 at 3:14













  • Can you please attach full size sample input image? Do you want to detect the boxes only or do you also want to segment the boxes on the basis of color?

    – ZdaR
    Nov 23 '18 at 4:13











  • You need to filter the image more precisely... Try using some edge detection open CV algo with dilating and eroding. To prevent whole box to be selected or only smaller parts ( round of 6 ) to be selected as contour just check area of every contour.... Opencv has facility to do this. Once you have area of each contour then drop the contour if it is very large or very small. This will filter all unnecessary contours and you will get the desired contours....

    – Devashish Prasad
    Nov 23 '18 at 4:14











  • @ZdaR I have edited the post and provided a large sample input image and sample output made in paint. I only want to detect the individual game tiles, with no concern for the colour or value within them

    – NightShade
    Nov 23 '18 at 4:38

















It looks like you are having boxes with defined colors at fixed positions. So, there is not much sense in detecting their position and they should be rather easy to identify if you filter for the right color(s).

– Klaus D.
Nov 23 '18 at 2:33





It looks like you are having boxes with defined colors at fixed positions. So, there is not much sense in detecting their position and they should be rather easy to identify if you filter for the right color(s).

– Klaus D.
Nov 23 '18 at 2:33













@KlausD. this was more of an exercise to determine if I could dynamically work out the position of the boxes as practice using cv2 - so if the boxes were layed out randomly, what the method would be to detect them. How would I go about filtering the colours if I were to do it using that method?

– NightShade
Nov 23 '18 at 3:14







@KlausD. this was more of an exercise to determine if I could dynamically work out the position of the boxes as practice using cv2 - so if the boxes were layed out randomly, what the method would be to detect them. How would I go about filtering the colours if I were to do it using that method?

– NightShade
Nov 23 '18 at 3:14















Can you please attach full size sample input image? Do you want to detect the boxes only or do you also want to segment the boxes on the basis of color?

– ZdaR
Nov 23 '18 at 4:13





Can you please attach full size sample input image? Do you want to detect the boxes only or do you also want to segment the boxes on the basis of color?

– ZdaR
Nov 23 '18 at 4:13













You need to filter the image more precisely... Try using some edge detection open CV algo with dilating and eroding. To prevent whole box to be selected or only smaller parts ( round of 6 ) to be selected as contour just check area of every contour.... Opencv has facility to do this. Once you have area of each contour then drop the contour if it is very large or very small. This will filter all unnecessary contours and you will get the desired contours....

– Devashish Prasad
Nov 23 '18 at 4:14





You need to filter the image more precisely... Try using some edge detection open CV algo with dilating and eroding. To prevent whole box to be selected or only smaller parts ( round of 6 ) to be selected as contour just check area of every contour.... Opencv has facility to do this. Once you have area of each contour then drop the contour if it is very large or very small. This will filter all unnecessary contours and you will get the desired contours....

– Devashish Prasad
Nov 23 '18 at 4:14













@ZdaR I have edited the post and provided a large sample input image and sample output made in paint. I only want to detect the individual game tiles, with no concern for the colour or value within them

– NightShade
Nov 23 '18 at 4:38





@ZdaR I have edited the post and provided a large sample input image and sample output made in paint. I only want to detect the individual game tiles, with no concern for the colour or value within them

– NightShade
Nov 23 '18 at 4:38












2 Answers
2






active

oldest

votes


















2














Rather than doing thresholding with the greyscale image, you can do thresholding on the color image with cv2.inRange. You can set the upper and lower bounds of allowed colors to include the numbered tiles but exclude the empty tiles and edges.



Also, I assume the step where you are checking if len(cnt) == 4: is to return only the square contours. However, resizing can result in contours that aren't exactly square for the tiles, and won't pass this check. Instead, you can get the outer contours of the tiles by changing the second input of findContours to 0 (contours = cv2.findContours(thresh, 0, 2)[1]) which sets the retrieval mode to cv2.RETR_EXTERNAL.



Here is the code with changes made, and appropriate upper and lower color bounds for the example image you gave.



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# inRange bounds
lower_bound = (0, 0, 210)
upper_bound = (230, 240, 250)

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))[:,:,:3]
resized_img = cv2.resize(img, (100, 100))
mask = cv2.inRange(resized_img, lower_bound, upper_bound)
contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


Here is the output image created:



Contoured 2048



Edit: Here is the code for using the example image directly:



import cv2
import numpy as np

img = cv2.imread('2048.jpg')
resized_img = cv2.resize(img, (100, 100))
lower_bound = (0,0,210)
upper_bound = (230,240,250)
mask = cv2.inRange(resized_img, lower_bound, upper_bound)

contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow('2048', resized_img)
cv2.waitKey(0)





share|improve this answer


























  • This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

    – NightShade
    Nov 23 '18 at 5:40











  • My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

    – A Kruger
    Nov 23 '18 at 5:45













  • it does not appear to be, I have tried using the code as you have submitted it above

    – NightShade
    Nov 23 '18 at 5:47











  • maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

    – NightShade
    Nov 23 '18 at 5:57






  • 1





    Thankyou that was it, works perfectly now :D been a massive help

    – NightShade
    Nov 23 '18 at 6:04





















0














One approach that you can take is to take the diff of this frame, from a snap-shot where all tiles are empty. This would get you the required mask with minimal computations.



Since you have not mentioned in the question that you have access to all empty tile snapshot, I would also present another technique known as color segmentation. Since the background color is consistent, but the numbered color tiles changes, So we will segment out the background first and then invert the mask to find contours as :



import cv2
import numpy as np


def threshold_tiles(board_img):
board_bgd_color_low = np.array([155, 170, 140])
board_bgd_color_high = np.array([200, 185, 195])

board_empty_low = np.array([175, 180, 200])
board_empty_high = np.array([185, 195, 210])

mask_bgd = cv2.inRange(board_img, board_bgd_color_low, board_bgd_color_high)
mask_tile = cv2.inRange(board_img, board_empty_low, board_empty_high)

mask = cv2.max(mask_bgd, mask_tile)

kernel = np.ones((7, 7), np.uint8)
mask = cv2.dilate(mask, kernel)

return ~mask


def get_box_contours(mask):
_, cnt, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cnt = filter(lambda x:cv2.contourArea(x) > 100, cnt)

return cnt


def main():
game_snapshot = cv2.imread("/path/to/img.jpg")

# Crop the white borders
game_snapshot = game_snapshot[5:-5, 5:-5]

mask = threshold_tiles(game_snapshot)
contours = get_box_contours(mask)

for i in xrange(len(contours)):
cv2.drawContours(game_snapshot, contours, i, (0, 255, 0), 3)

cv2.imwrite("output.png", game_snapshot)


if __name__ == "__main__":
main()


Intermediate masks:



enter image description here + enter image description here = enter image description here



Final mask for contours:



enter image description here



Output:



enter image description here






share|improve this answer


























  • Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

    – NightShade
    Nov 23 '18 at 5:56











  • No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

    – ZdaR
    Nov 23 '18 at 6:26











Your Answer






StackExchange.ifUsing("editor", function () {
StackExchange.using("externalEditor", function () {
StackExchange.using("snippets", function () {
StackExchange.snippets.init();
});
});
}, "code-snippets");

StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "1"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);

StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});

function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: true,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: 10,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});


}
});














draft saved

draft discarded


















StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53439913%2fdetecting-tile-squares-in-2048-with-cv2%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown

























2 Answers
2






active

oldest

votes








2 Answers
2






active

oldest

votes









active

oldest

votes






active

oldest

votes









2














Rather than doing thresholding with the greyscale image, you can do thresholding on the color image with cv2.inRange. You can set the upper and lower bounds of allowed colors to include the numbered tiles but exclude the empty tiles and edges.



Also, I assume the step where you are checking if len(cnt) == 4: is to return only the square contours. However, resizing can result in contours that aren't exactly square for the tiles, and won't pass this check. Instead, you can get the outer contours of the tiles by changing the second input of findContours to 0 (contours = cv2.findContours(thresh, 0, 2)[1]) which sets the retrieval mode to cv2.RETR_EXTERNAL.



Here is the code with changes made, and appropriate upper and lower color bounds for the example image you gave.



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# inRange bounds
lower_bound = (0, 0, 210)
upper_bound = (230, 240, 250)

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))[:,:,:3]
resized_img = cv2.resize(img, (100, 100))
mask = cv2.inRange(resized_img, lower_bound, upper_bound)
contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


Here is the output image created:



Contoured 2048



Edit: Here is the code for using the example image directly:



import cv2
import numpy as np

img = cv2.imread('2048.jpg')
resized_img = cv2.resize(img, (100, 100))
lower_bound = (0,0,210)
upper_bound = (230,240,250)
mask = cv2.inRange(resized_img, lower_bound, upper_bound)

contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow('2048', resized_img)
cv2.waitKey(0)





share|improve this answer


























  • This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

    – NightShade
    Nov 23 '18 at 5:40











  • My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

    – A Kruger
    Nov 23 '18 at 5:45













  • it does not appear to be, I have tried using the code as you have submitted it above

    – NightShade
    Nov 23 '18 at 5:47











  • maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

    – NightShade
    Nov 23 '18 at 5:57






  • 1





    Thankyou that was it, works perfectly now :D been a massive help

    – NightShade
    Nov 23 '18 at 6:04


















2














Rather than doing thresholding with the greyscale image, you can do thresholding on the color image with cv2.inRange. You can set the upper and lower bounds of allowed colors to include the numbered tiles but exclude the empty tiles and edges.



Also, I assume the step where you are checking if len(cnt) == 4: is to return only the square contours. However, resizing can result in contours that aren't exactly square for the tiles, and won't pass this check. Instead, you can get the outer contours of the tiles by changing the second input of findContours to 0 (contours = cv2.findContours(thresh, 0, 2)[1]) which sets the retrieval mode to cv2.RETR_EXTERNAL.



Here is the code with changes made, and appropriate upper and lower color bounds for the example image you gave.



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# inRange bounds
lower_bound = (0, 0, 210)
upper_bound = (230, 240, 250)

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))[:,:,:3]
resized_img = cv2.resize(img, (100, 100))
mask = cv2.inRange(resized_img, lower_bound, upper_bound)
contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


Here is the output image created:



Contoured 2048



Edit: Here is the code for using the example image directly:



import cv2
import numpy as np

img = cv2.imread('2048.jpg')
resized_img = cv2.resize(img, (100, 100))
lower_bound = (0,0,210)
upper_bound = (230,240,250)
mask = cv2.inRange(resized_img, lower_bound, upper_bound)

contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow('2048', resized_img)
cv2.waitKey(0)





share|improve this answer


























  • This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

    – NightShade
    Nov 23 '18 at 5:40











  • My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

    – A Kruger
    Nov 23 '18 at 5:45













  • it does not appear to be, I have tried using the code as you have submitted it above

    – NightShade
    Nov 23 '18 at 5:47











  • maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

    – NightShade
    Nov 23 '18 at 5:57






  • 1





    Thankyou that was it, works perfectly now :D been a massive help

    – NightShade
    Nov 23 '18 at 6:04
















2












2








2







Rather than doing thresholding with the greyscale image, you can do thresholding on the color image with cv2.inRange. You can set the upper and lower bounds of allowed colors to include the numbered tiles but exclude the empty tiles and edges.



Also, I assume the step where you are checking if len(cnt) == 4: is to return only the square contours. However, resizing can result in contours that aren't exactly square for the tiles, and won't pass this check. Instead, you can get the outer contours of the tiles by changing the second input of findContours to 0 (contours = cv2.findContours(thresh, 0, 2)[1]) which sets the retrieval mode to cv2.RETR_EXTERNAL.



Here is the code with changes made, and appropriate upper and lower color bounds for the example image you gave.



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# inRange bounds
lower_bound = (0, 0, 210)
upper_bound = (230, 240, 250)

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))[:,:,:3]
resized_img = cv2.resize(img, (100, 100))
mask = cv2.inRange(resized_img, lower_bound, upper_bound)
contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


Here is the output image created:



Contoured 2048



Edit: Here is the code for using the example image directly:



import cv2
import numpy as np

img = cv2.imread('2048.jpg')
resized_img = cv2.resize(img, (100, 100))
lower_bound = (0,0,210)
upper_bound = (230,240,250)
mask = cv2.inRange(resized_img, lower_bound, upper_bound)

contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow('2048', resized_img)
cv2.waitKey(0)





share|improve this answer















Rather than doing thresholding with the greyscale image, you can do thresholding on the color image with cv2.inRange. You can set the upper and lower bounds of allowed colors to include the numbered tiles but exclude the empty tiles and edges.



Also, I assume the step where you are checking if len(cnt) == 4: is to return only the square contours. However, resizing can result in contours that aren't exactly square for the tiles, and won't pass this check. Instead, you can get the outer contours of the tiles by changing the second input of findContours to 0 (contours = cv2.findContours(thresh, 0, 2)[1]) which sets the retrieval mode to cv2.RETR_EXTERNAL.



Here is the code with changes made, and appropriate upper and lower color bounds for the example image you gave.



import cv2
import mss
import time
import numpy as np

# Static screenshot for board
monitor = {"top": 135, "left": 425, "width": 500, "height": 500}
sct = mss.mss()

# inRange bounds
lower_bound = (0, 0, 210)
upper_bound = (230, 240, 250)

# Run for a maximum of 150s or until 'q' is pressed
last_time = time.time()
while time.time() - last_time < 150:
img = np.asarray(sct.grab(monitor))[:,:,:3]
resized_img = cv2.resize(img, (100, 100))
mask = cv2.inRange(resized_img, lower_bound, upper_bound)
contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow("2048", resized_img)

if cv2.waitKey(25) & 0xFF == ord("q"):
break

cv2.destroyAllWindows()


Here is the output image created:



Contoured 2048



Edit: Here is the code for using the example image directly:



import cv2
import numpy as np

img = cv2.imread('2048.jpg')
resized_img = cv2.resize(img, (100, 100))
lower_bound = (0,0,210)
upper_bound = (230,240,250)
mask = cv2.inRange(resized_img, lower_bound, upper_bound)

contours = cv2.findContours(mask, 0, 2)[1]
for cnt in contours:
cv2.drawContours(resized_img, [cnt], 0, (0, 255, 0), 2)
cv2.imshow('2048', resized_img)
cv2.waitKey(0)






share|improve this answer














share|improve this answer



share|improve this answer








edited Nov 23 '18 at 5:58

























answered Nov 23 '18 at 5:19









A KrugerA Kruger

1,37838




1,37838













  • This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

    – NightShade
    Nov 23 '18 at 5:40











  • My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

    – A Kruger
    Nov 23 '18 at 5:45













  • it does not appear to be, I have tried using the code as you have submitted it above

    – NightShade
    Nov 23 '18 at 5:47











  • maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

    – NightShade
    Nov 23 '18 at 5:57






  • 1





    Thankyou that was it, works perfectly now :D been a massive help

    – NightShade
    Nov 23 '18 at 6:04





















  • This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

    – NightShade
    Nov 23 '18 at 5:40











  • My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

    – A Kruger
    Nov 23 '18 at 5:45













  • it does not appear to be, I have tried using the code as you have submitted it above

    – NightShade
    Nov 23 '18 at 5:47











  • maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

    – NightShade
    Nov 23 '18 at 5:57






  • 1





    Thankyou that was it, works perfectly now :D been a massive help

    – NightShade
    Nov 23 '18 at 6:04



















This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

– NightShade
Nov 23 '18 at 5:40





This does seem like the short most simple solution and I love it :) however when I try to run it myself I get an error: mask = cv2.inRange(resized_img, lower_bound, upper_bound) cv2.error: OpenCV(3.4.3) C:projectsopencv-pythonopencvmodulescoresrcarithm.cpp:1778: error: (-209:Sizes of input arguments do not match) The lower boundary is neither an array of the same size and same type as src, nor a scalar in function 'cv::inRange'

– NightShade
Nov 23 '18 at 5:40













My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

– A Kruger
Nov 23 '18 at 5:45







My first guess is the resized_img is a greyscale image, not the color image. Can you check if that's the case? It needs to be a 3-channel image.

– A Kruger
Nov 23 '18 at 5:45















it does not appear to be, I have tried using the code as you have submitted it above

– NightShade
Nov 23 '18 at 5:47





it does not appear to be, I have tried using the code as you have submitted it above

– NightShade
Nov 23 '18 at 5:47













maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

– NightShade
Nov 23 '18 at 5:57





maybe it has something to do with the format of the screenshot taken by mss, the edited code for changing the image directly works perfectly fine

– NightShade
Nov 23 '18 at 5:57




1




1





Thankyou that was it, works perfectly now :D been a massive help

– NightShade
Nov 23 '18 at 6:04







Thankyou that was it, works perfectly now :D been a massive help

– NightShade
Nov 23 '18 at 6:04















0














One approach that you can take is to take the diff of this frame, from a snap-shot where all tiles are empty. This would get you the required mask with minimal computations.



Since you have not mentioned in the question that you have access to all empty tile snapshot, I would also present another technique known as color segmentation. Since the background color is consistent, but the numbered color tiles changes, So we will segment out the background first and then invert the mask to find contours as :



import cv2
import numpy as np


def threshold_tiles(board_img):
board_bgd_color_low = np.array([155, 170, 140])
board_bgd_color_high = np.array([200, 185, 195])

board_empty_low = np.array([175, 180, 200])
board_empty_high = np.array([185, 195, 210])

mask_bgd = cv2.inRange(board_img, board_bgd_color_low, board_bgd_color_high)
mask_tile = cv2.inRange(board_img, board_empty_low, board_empty_high)

mask = cv2.max(mask_bgd, mask_tile)

kernel = np.ones((7, 7), np.uint8)
mask = cv2.dilate(mask, kernel)

return ~mask


def get_box_contours(mask):
_, cnt, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cnt = filter(lambda x:cv2.contourArea(x) > 100, cnt)

return cnt


def main():
game_snapshot = cv2.imread("/path/to/img.jpg")

# Crop the white borders
game_snapshot = game_snapshot[5:-5, 5:-5]

mask = threshold_tiles(game_snapshot)
contours = get_box_contours(mask)

for i in xrange(len(contours)):
cv2.drawContours(game_snapshot, contours, i, (0, 255, 0), 3)

cv2.imwrite("output.png", game_snapshot)


if __name__ == "__main__":
main()


Intermediate masks:



enter image description here + enter image description here = enter image description here



Final mask for contours:



enter image description here



Output:



enter image description here






share|improve this answer


























  • Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

    – NightShade
    Nov 23 '18 at 5:56











  • No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

    – ZdaR
    Nov 23 '18 at 6:26
















0














One approach that you can take is to take the diff of this frame, from a snap-shot where all tiles are empty. This would get you the required mask with minimal computations.



Since you have not mentioned in the question that you have access to all empty tile snapshot, I would also present another technique known as color segmentation. Since the background color is consistent, but the numbered color tiles changes, So we will segment out the background first and then invert the mask to find contours as :



import cv2
import numpy as np


def threshold_tiles(board_img):
board_bgd_color_low = np.array([155, 170, 140])
board_bgd_color_high = np.array([200, 185, 195])

board_empty_low = np.array([175, 180, 200])
board_empty_high = np.array([185, 195, 210])

mask_bgd = cv2.inRange(board_img, board_bgd_color_low, board_bgd_color_high)
mask_tile = cv2.inRange(board_img, board_empty_low, board_empty_high)

mask = cv2.max(mask_bgd, mask_tile)

kernel = np.ones((7, 7), np.uint8)
mask = cv2.dilate(mask, kernel)

return ~mask


def get_box_contours(mask):
_, cnt, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cnt = filter(lambda x:cv2.contourArea(x) > 100, cnt)

return cnt


def main():
game_snapshot = cv2.imread("/path/to/img.jpg")

# Crop the white borders
game_snapshot = game_snapshot[5:-5, 5:-5]

mask = threshold_tiles(game_snapshot)
contours = get_box_contours(mask)

for i in xrange(len(contours)):
cv2.drawContours(game_snapshot, contours, i, (0, 255, 0), 3)

cv2.imwrite("output.png", game_snapshot)


if __name__ == "__main__":
main()


Intermediate masks:



enter image description here + enter image description here = enter image description here



Final mask for contours:



enter image description here



Output:



enter image description here






share|improve this answer


























  • Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

    – NightShade
    Nov 23 '18 at 5:56











  • No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

    – ZdaR
    Nov 23 '18 at 6:26














0












0








0







One approach that you can take is to take the diff of this frame, from a snap-shot where all tiles are empty. This would get you the required mask with minimal computations.



Since you have not mentioned in the question that you have access to all empty tile snapshot, I would also present another technique known as color segmentation. Since the background color is consistent, but the numbered color tiles changes, So we will segment out the background first and then invert the mask to find contours as :



import cv2
import numpy as np


def threshold_tiles(board_img):
board_bgd_color_low = np.array([155, 170, 140])
board_bgd_color_high = np.array([200, 185, 195])

board_empty_low = np.array([175, 180, 200])
board_empty_high = np.array([185, 195, 210])

mask_bgd = cv2.inRange(board_img, board_bgd_color_low, board_bgd_color_high)
mask_tile = cv2.inRange(board_img, board_empty_low, board_empty_high)

mask = cv2.max(mask_bgd, mask_tile)

kernel = np.ones((7, 7), np.uint8)
mask = cv2.dilate(mask, kernel)

return ~mask


def get_box_contours(mask):
_, cnt, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cnt = filter(lambda x:cv2.contourArea(x) > 100, cnt)

return cnt


def main():
game_snapshot = cv2.imread("/path/to/img.jpg")

# Crop the white borders
game_snapshot = game_snapshot[5:-5, 5:-5]

mask = threshold_tiles(game_snapshot)
contours = get_box_contours(mask)

for i in xrange(len(contours)):
cv2.drawContours(game_snapshot, contours, i, (0, 255, 0), 3)

cv2.imwrite("output.png", game_snapshot)


if __name__ == "__main__":
main()


Intermediate masks:



enter image description here + enter image description here = enter image description here



Final mask for contours:



enter image description here



Output:



enter image description here






share|improve this answer















One approach that you can take is to take the diff of this frame, from a snap-shot where all tiles are empty. This would get you the required mask with minimal computations.



Since you have not mentioned in the question that you have access to all empty tile snapshot, I would also present another technique known as color segmentation. Since the background color is consistent, but the numbered color tiles changes, So we will segment out the background first and then invert the mask to find contours as :



import cv2
import numpy as np


def threshold_tiles(board_img):
board_bgd_color_low = np.array([155, 170, 140])
board_bgd_color_high = np.array([200, 185, 195])

board_empty_low = np.array([175, 180, 200])
board_empty_high = np.array([185, 195, 210])

mask_bgd = cv2.inRange(board_img, board_bgd_color_low, board_bgd_color_high)
mask_tile = cv2.inRange(board_img, board_empty_low, board_empty_high)

mask = cv2.max(mask_bgd, mask_tile)

kernel = np.ones((7, 7), np.uint8)
mask = cv2.dilate(mask, kernel)

return ~mask


def get_box_contours(mask):
_, cnt, hierarchy = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_NONE)

cnt = filter(lambda x:cv2.contourArea(x) > 100, cnt)

return cnt


def main():
game_snapshot = cv2.imread("/path/to/img.jpg")

# Crop the white borders
game_snapshot = game_snapshot[5:-5, 5:-5]

mask = threshold_tiles(game_snapshot)
contours = get_box_contours(mask)

for i in xrange(len(contours)):
cv2.drawContours(game_snapshot, contours, i, (0, 255, 0), 3)

cv2.imwrite("output.png", game_snapshot)


if __name__ == "__main__":
main()


Intermediate masks:



enter image description here + enter image description here = enter image description here



Final mask for contours:



enter image description here



Output:



enter image description here







share|improve this answer














share|improve this answer



share|improve this answer








edited Nov 23 '18 at 5:31

























answered Nov 23 '18 at 5:25









ZdaRZdaR

13.4k33554




13.4k33554













  • Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

    – NightShade
    Nov 23 '18 at 5:56











  • No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

    – ZdaR
    Nov 23 '18 at 6:26



















  • Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

    – NightShade
    Nov 23 '18 at 5:56











  • No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

    – ZdaR
    Nov 23 '18 at 6:26

















Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

– NightShade
Nov 23 '18 at 5:56





Is there a reason the edges like in 128 are not as straight for this? does this have something to do with the dilation

– NightShade
Nov 23 '18 at 5:56













No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

– ZdaR
Nov 23 '18 at 6:26





No, it is due to the .jpg format of the input image, which is a loosy format and interpolates the pixel values, it would have been better if you would have used .png format for image serialization.

– ZdaR
Nov 23 '18 at 6:26


















draft saved

draft discarded




















































Thanks for contributing an answer to Stack Overflow!


  • Please be sure to answer the question. Provide details and share your research!

But avoid



  • Asking for help, clarification, or responding to other answers.

  • Making statements based on opinion; back them up with references or personal experience.


To learn more, see our tips on writing great answers.




draft saved


draft discarded














StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fstackoverflow.com%2fquestions%2f53439913%2fdetecting-tile-squares-in-2048-with-cv2%23new-answer', 'question_page');
}
);

Post as a guest















Required, but never shown





















































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown

































Required, but never shown














Required, but never shown












Required, but never shown







Required, but never shown







這個網誌中的熱門文章

Tangent Lines Diagram Along Smooth Curve

Yusuf al-Mu'taman ibn Hud

Zucchini