initial commit
commit
7dcccad1c9
@ -0,0 +1,15 @@
|
||||
# Local files
|
||||
.env
|
||||
*.local*
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
|
||||
# Binaries
|
||||
bin/
|
||||
.out/
|
||||
out/
|
||||
dist/
|
||||
|
||||
# Editors
|
||||
.vscode/
|
@ -0,0 +1,36 @@
|
||||
# OpenCV Maze Solver
|
||||
|
||||
<p align="center">
|
||||
<img src="images/IMG_0867.jpg" width="300" />
|
||||
</p>
|
||||
|
||||
This is a simple maze solver using OpenCV and Python. The maze is solved using the A* algorithm from the NetworkX library.
|
||||
|
||||
## Installation
|
||||
|
||||
This project uses Poetry for dependency management. To install the dependencies, run the following command:
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
### Image
|
||||
|
||||
To run the maze solver, use the following command:
|
||||
|
||||
```bash
|
||||
poetry run python main.py --image <image_path>
|
||||
```
|
||||
|
||||
Replace `<image_path>` with the path to the maze image you want to solve like `images/IMG_0867.jpg`.
|
||||
|
||||
|
||||
### Camera
|
||||
|
||||
To run the maze solver using the camera, use the following command:
|
||||
|
||||
```bash
|
||||
poetry run python main_camera.py
|
||||
```
|
@ -0,0 +1,161 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import utils
|
||||
|
||||
|
||||
def solve_maze(image, debug=False):
|
||||
original_width, original_height = image.shape[1], image.shape[0]
|
||||
image = cv2.resize(image, (4000, 3000))
|
||||
|
||||
def debug_image(label, image, *args, **kwargs):
|
||||
if debug:
|
||||
utils.display_image(label, image, *args, **kwargs)
|
||||
|
||||
debug_image("Image", image)
|
||||
|
||||
# convert the image to grayscale
|
||||
image_gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
|
||||
|
||||
# extract the tags
|
||||
tag_registry = utils.extract_tags_dict_single(image_gray)
|
||||
|
||||
missing_tags = {1, 2, 3, 4} - set(tag_registry.keys())
|
||||
if len(missing_tags) > 0:
|
||||
raise Exception(f"Missing tags in given image: {missing_tags}")
|
||||
|
||||
# extract the foreground
|
||||
normalized_image = utils.extract_foreground(image_gray)
|
||||
debug_image("Normalized", normalized_image)
|
||||
|
||||
# draw a black rectangle around the detected tags
|
||||
for r in tag_registry.values():
|
||||
(ptA, ptB, ptC, ptD) = r.corners
|
||||
|
||||
# get min_x, min_y, max_x, max_y
|
||||
min_x = min(ptA[0], ptB[0], ptC[0], ptD[0])
|
||||
min_y = min(ptA[1], ptB[1], ptC[1], ptD[1])
|
||||
max_x = max(ptA[0], ptB[0], ptC[0], ptD[0])
|
||||
max_y = max(ptA[1], ptB[1], ptC[1], ptD[1])
|
||||
|
||||
width = max_x - min_x
|
||||
height = max_y - min_y
|
||||
|
||||
# enlarge the rectangle
|
||||
min_x -= (width / 7) * 1.5
|
||||
min_y -= (height * 2 / 7) * 1.5
|
||||
max_x += (width / 7) * 1.5
|
||||
max_y += (height / 7) * 1.5
|
||||
|
||||
# draw the rectangle
|
||||
cv2.rectangle(normalized_image, (int(min_x), int(min_y)), (int(max_x), int(max_y)), (255, 255, 255), -1)
|
||||
|
||||
# cv2.fillPoly(image, [np.int32(r.corners)], (0, 0, 0))
|
||||
|
||||
# erode the image
|
||||
eroded = cv2.erode(normalized_image, np.ones((7, 7), np.uint8))
|
||||
# eroded = cv2.GaussianBlur(eroded, (15, 15), 0)
|
||||
|
||||
debug_image("Eroded", eroded)
|
||||
|
||||
# binarize the image
|
||||
_, thresh = cv2.threshold(eroded, 150, 255, cv2.THRESH_BINARY)
|
||||
thresh = cv2.bitwise_not(thresh)
|
||||
|
||||
debug_image("Threshold", thresh)
|
||||
|
||||
dilated = cv2.dilate(thresh, None, iterations=8)
|
||||
|
||||
debug_image("Dilated", dilated)
|
||||
|
||||
# crop the maze region using tag 1 and 2 as bounding box
|
||||
tag_1_center = tag_registry[1].center
|
||||
tag_2_center = tag_registry[2].center
|
||||
|
||||
min_x = min(tag_1_center[0], tag_2_center[0])
|
||||
min_y = min(tag_1_center[1], tag_2_center[1])
|
||||
max_x = max(tag_1_center[0], tag_2_center[0])
|
||||
max_y = max(tag_1_center[1], tag_2_center[1])
|
||||
|
||||
def transform_1(x, y):
|
||||
return x - min_x, y - min_y
|
||||
|
||||
cropped_image = dilated[int(min_y) : int(max_y), int(min_x) : int(max_x)]
|
||||
|
||||
cropped_image = cv2.cvtColor(cropped_image, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
cv2.circle(cropped_image, np.int32(transform_1(*tag_registry[3].center)), 10, (0, 0, 255), -1)
|
||||
cv2.circle(cropped_image, np.int32(transform_1(*tag_registry[4].center)), 10, (0, 255, 0), -1)
|
||||
|
||||
debug_image("Cropped", cropped_image)
|
||||
|
||||
def transform_2(x, y):
|
||||
return int((x - min_x) * 256 / (max_x - min_x)), int((y - min_y) * 256 / (max_y - min_y))
|
||||
|
||||
maze_image_small = cv2.resize(cropped_image, (256, 256), interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
debug_image("Maze", maze_image_small, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
# convert the maze image to a numpy bitmap
|
||||
|
||||
maze_bitmap = np.zeros((256, 256), dtype=np.uint8)
|
||||
|
||||
for x in range(256):
|
||||
for y in range(256):
|
||||
if maze_image_small[y, x, 0] > 0:
|
||||
maze_bitmap[y, x] = 1
|
||||
|
||||
utils.display_image("Maze Bitmap", maze_bitmap * 255, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
# use networkx to solve the maze
|
||||
|
||||
import networkx as nx
|
||||
from networkx import Graph
|
||||
|
||||
# def transform_3(x, y):
|
||||
# pt = transform_2(x, y)
|
||||
# return pt[0] // 2, pt[1] // 2
|
||||
|
||||
G: Graph = nx.grid_2d_graph(256, 256)
|
||||
G.add_edges_from(
|
||||
[((x, y), (x + 1, y + 1)) for x in range(255) for y in range(255)]
|
||||
+ [((x + 1, y), (x, y + 1)) for x in range(255) for y in range(255)],
|
||||
weight=1.4,
|
||||
)
|
||||
|
||||
for x in range(256):
|
||||
for y in range(256):
|
||||
if maze_bitmap[y, x] == 1:
|
||||
G.remove_node((x, y))
|
||||
|
||||
start_node = transform_2(*tag_registry[3].center)
|
||||
end_node = transform_2(*tag_registry[4].center)
|
||||
|
||||
print(f"Start: {start_node}, End: {end_node}")
|
||||
|
||||
def dist(a, b):
|
||||
(x1, y1) = a
|
||||
(x2, y2) = b
|
||||
return ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5
|
||||
|
||||
try:
|
||||
path = nx.astar_path(G, start_node, end_node, heuristic=dist)
|
||||
except nx.NetworkXNoPath:
|
||||
raise Exception("No path found")
|
||||
|
||||
print(f"Shortest path length: {len(path)}")
|
||||
|
||||
# draw the path on the maze image
|
||||
for x, y in path:
|
||||
maze_image_small[y, x] = [0, 255, 0]
|
||||
|
||||
debug_image("Maze Path", maze_image_small, interpolation=cv2.INTER_NEAREST)
|
||||
|
||||
def un_transform(x, y):
|
||||
out_x = int(x * (max_x - min_x) / 256 + min_x)
|
||||
out_y = int(y * (max_y - min_y) / 256 + min_y)
|
||||
return int(out_x * original_width / 4000), int(out_y * original_height / 3000)
|
||||
|
||||
path = [un_transform(x, y) for x, y in path]
|
||||
|
||||
return path
|
Binary file not shown.
After Width: | Height: | Size: 2.2 MiB |
Binary file not shown.
After Width: | Height: | Size: 2.3 MiB |
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
Binary file not shown.
After Width: | Height: | Size: 3.4 MiB |
@ -0,0 +1,29 @@
|
||||
# import the necessary packages
|
||||
import cv2
|
||||
|
||||
import utils
|
||||
import cv_maze
|
||||
|
||||
import argparse
|
||||
|
||||
# construct the argument parser and parse the arguments
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("-i", "--image", required=True, help="path to input image containing AprilTag")
|
||||
args = vars(ap.parse_args())
|
||||
|
||||
# load the input image and convert it to grayscale
|
||||
print("[INFO] loading image...")
|
||||
|
||||
image = cv2.imread(args["image"])
|
||||
utils.display_image("Image", image)
|
||||
|
||||
path = cv_maze.solve_maze(image)
|
||||
|
||||
# draw the path on the original image
|
||||
|
||||
for i in range(len(path) - 1):
|
||||
cv2.line(image, path[i], path[i + 1], (0, 255, 0), 5)
|
||||
|
||||
utils.display_image("Solution", image)
|
||||
|
||||
utils.wait_close_app()
|
@ -0,0 +1,30 @@
|
||||
import cv2
|
||||
|
||||
import utils
|
||||
import cv_maze
|
||||
|
||||
camera = cv2.VideoCapture(0)
|
||||
camera.set(cv2.CAP_PROP_FRAME_WIDTH, 1280)
|
||||
camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 720)
|
||||
|
||||
while utils.wait_frame():
|
||||
ret, frame = camera.read()
|
||||
|
||||
if not ret:
|
||||
break
|
||||
|
||||
utils.display_image("Camera", frame)
|
||||
|
||||
try:
|
||||
path = cv_maze.solve_maze(frame)
|
||||
except Exception as e:
|
||||
print(e)
|
||||
continue
|
||||
|
||||
# draw the path on the original image
|
||||
for i in range(len(path) - 1):
|
||||
cv2.line(frame, path[i], path[i + 1], (0, 255, 0), 5)
|
||||
|
||||
utils.display_image("Solution", frame)
|
||||
|
||||
camera.release()
|
@ -0,0 +1,125 @@
|
||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "apriltag"
|
||||
version = "0.0.16"
|
||||
description = "apriltag marker detection"
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "apriltag-0.0.16.tar.gz", hash = "sha256:151e37f5a88bb81726e2bda82938d7e22844131d5894ed9f30eabcd3b8beb071"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dt-apriltags"
|
||||
version = "3.1.7"
|
||||
description = ""
|
||||
optional = false
|
||||
python-versions = "*"
|
||||
files = [
|
||||
{file = "dt_apriltags-3.1.7-py2-none-linux_armv7l.whl", hash = "sha256:dd0b3b99ec6a9cc1541897f1a4d1896f2f052399cecc91421b69a1d737d86991"},
|
||||
{file = "dt_apriltags-3.1.7-py2-none-manylinux2010_x86_64.whl", hash = "sha256:987c6f9f9b9d5c324fac363b074f744b9eeb913065152499aa3dd828c2b1883a"},
|
||||
{file = "dt_apriltags-3.1.7-py2-none-manylinux2014_aarch64.whl", hash = "sha256:3b0b78c4e86031dbf1ce4ce68f2de9ed6587edd0da1b6fdb138cf67130914f1d"},
|
||||
{file = "dt_apriltags-3.1.7-py3-none-linux_armv7l.whl", hash = "sha256:ffef2ec5b67d8419d92ef8cbcec18bc131c1c6a8a33f441cf08d44ea06fb501a"},
|
||||
{file = "dt_apriltags-3.1.7-py3-none-manylinux2010_x86_64.whl", hash = "sha256:946fc0cf9a5c6d884ca35e693268aac6248b78717749c93e92a516a85746f0ea"},
|
||||
{file = "dt_apriltags-3.1.7-py3-none-manylinux2014_aarch64.whl", hash = "sha256:9e149899b72291619adc13424343837714ad0d6d80d7878168e1fa0248df4262"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = "*"
|
||||
|
||||
[[package]]
|
||||
name = "networkx"
|
||||
version = "3.3"
|
||||
description = "Python package for creating and manipulating graphs and networks"
|
||||
optional = false
|
||||
python-versions = ">=3.10"
|
||||
files = [
|
||||
{file = "networkx-3.3-py3-none-any.whl", hash = "sha256:28575580c6ebdaf4505b22c6256a2b9de86b316dc63ba9e93abde3d78dfdbcf2"},
|
||||
{file = "networkx-3.3.tar.gz", hash = "sha256:0c127d8b2f4865f59ae9cb8aafcd60b5c70f3241ebd66f7defad7c4ab90126c9"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
default = ["matplotlib (>=3.6)", "numpy (>=1.23)", "pandas (>=1.4)", "scipy (>=1.9,!=1.11.0,!=1.11.1)"]
|
||||
developer = ["changelist (==0.5)", "mypy (>=1.1)", "pre-commit (>=3.2)", "rtoml"]
|
||||
doc = ["myst-nb (>=1.0)", "numpydoc (>=1.7)", "pillow (>=9.4)", "pydata-sphinx-theme (>=0.14)", "sphinx (>=7)", "sphinx-gallery (>=0.14)", "texext (>=0.6.7)"]
|
||||
extra = ["lxml (>=4.6)", "pydot (>=2.0)", "pygraphviz (>=1.12)", "sympy (>=1.10)"]
|
||||
test = ["pytest (>=7.2)", "pytest-cov (>=4.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "numpy"
|
||||
version = "2.0.1"
|
||||
description = "Fundamental package for array computing in Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
files = [
|
||||
{file = "numpy-2.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fbb536eac80e27a2793ffd787895242b7f18ef792563d742c2d673bfcb75134"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:69ff563d43c69b1baba77af455dd0a839df8d25e8590e79c90fcbe1499ebde42"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:1b902ce0e0a5bb7704556a217c4f63a7974f8f43e090aff03fcf262e0b135e02"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:f1659887361a7151f89e79b276ed8dff3d75877df906328f14d8bb40bb4f5101"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4658c398d65d1b25e1760de3157011a80375da861709abd7cef3bad65d6543f9"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4127d4303b9ac9f94ca0441138acead39928938660ca58329fe156f84b9f3015"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e5eeca8067ad04bc8a2a8731183d51d7cbaac66d86085d5f4766ee6bf19c7f87"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adbd9bb520c866e1bfd7e10e1880a1f7749f1f6e5017686a5fbb9b72cf69f82"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-win32.whl", hash = "sha256:7b9853803278db3bdcc6cd5beca37815b133e9e77ff3d4733c247414e78eb8d1"},
|
||||
{file = "numpy-2.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:81b0893a39bc5b865b8bf89e9ad7807e16717f19868e9d234bdaf9b1f1393868"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75b4e316c5902d8163ef9d423b1c3f2f6252226d1aa5cd8a0a03a7d01ffc6268"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6e4eeb6eb2fced786e32e6d8df9e755ce5be920d17f7ce00bc38fcde8ccdbf9e"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a1e01dcaab205fbece13c1410253a9eea1b1c9b61d237b6fa59bcc46e8e89343"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:a8fc2de81ad835d999113ddf87d1ea2b0f4704cbd947c948d2f5513deafe5a7b"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a3d94942c331dd4e0e1147f7a8699a4aa47dffc11bf8a1523c12af8b2e91bbe"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15eb4eca47d36ec3f78cde0a3a2ee24cf05ca7396ef808dda2c0ddad7c2bde67"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:b83e16a5511d1b1f8a88cbabb1a6f6a499f82c062a4251892d9ad5d609863fb7"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f87fec1f9bc1efd23f4227becff04bd0e979e23ca50cc92ec88b38489db3b55"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-win32.whl", hash = "sha256:36d3a9405fd7c511804dc56fc32974fa5533bdeb3cd1604d6b8ff1d292b819c4"},
|
||||
{file = "numpy-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:08458fbf403bff5e2b45f08eda195d4b0c9b35682311da5a5a0a0925b11b9bd8"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6bf4e6f4a2a2e26655717a1983ef6324f2664d7011f6ef7482e8c0b3d51e82ac"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6fddc5fe258d3328cd8e3d7d3e02234c5d70e01ebe377a6ab92adb14039cb4"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:5daab361be6ddeb299a918a7c0864fa8618af66019138263247af405018b04e1"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:ea2326a4dca88e4a274ba3a4405eb6c6467d3ffbd8c7d38632502eaae3820587"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529af13c5f4b7a932fb0e1911d3a75da204eff023ee5e0e79c1751564221a5c8"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6790654cb13eab303d8402354fabd47472b24635700f631f041bd0b65e37298a"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbab9fc9c391700e3e1287666dfd82d8666d10e69a6c4a09ab97574c0b7ee0a7"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99d0d92a5e3613c33a5f01db206a33f8fdf3d71f2912b0de1739894668b7a93b"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-win32.whl", hash = "sha256:173a00b9995f73b79eb0191129f2455f1e34c203f559dd118636858cc452a1bf"},
|
||||
{file = "numpy-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:bb2124fdc6e62baae159ebcfa368708867eb56806804d005860b6007388df171"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:bfc085b28d62ff4009364e7ca34b80a9a080cbd97c2c0630bb5f7f770dae9414"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8fae4ebbf95a179c1156fab0b142b74e4ba4204c87bde8d3d8b6f9c34c5825ef"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-macosx_14_0_arm64.whl", hash = "sha256:72dc22e9ec8f6eaa206deb1b1355eb2e253899d7347f5e2fae5f0af613741d06"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-macosx_14_0_x86_64.whl", hash = "sha256:ec87f5f8aca726117a1c9b7083e7656a9d0d606eec7299cc067bb83d26f16e0c"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f682ea61a88479d9498bf2091fdcd722b090724b08b31d63e022adc063bad59"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8efc84f01c1cd7e34b3fb310183e72fcdf55293ee736d679b6d35b35d80bba26"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:3fdabe3e2a52bc4eff8dc7a5044342f8bd9f11ef0934fcd3289a788c0eb10018"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:24a0e1befbfa14615b49ba9659d3d8818a0f4d8a1c5822af8696706fbda7310c"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-win32.whl", hash = "sha256:f9cf5ea551aec449206954b075db819f52adc1638d46a6738253a712d553c7b4"},
|
||||
{file = "numpy-2.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:e9e81fa9017eaa416c056e5d9e71be93d05e2c3c2ab308d23307a8bc4443c368"},
|
||||
{file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:61728fba1e464f789b11deb78a57805c70b2ed02343560456190d0501ba37b0f"},
|
||||
{file = "numpy-2.0.1-pp39-pypy39_pp73-macosx_14_0_x86_64.whl", hash = "sha256:12f5d865d60fb9734e60a60f1d5afa6d962d8d4467c120a1c0cda6eb2964437d"},
|
||||
{file = "numpy-2.0.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eacf3291e263d5a67d8c1a581a8ebbcfd6447204ef58828caf69a5e3e8c75990"},
|
||||
{file = "numpy-2.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2c3a346ae20cfd80b6cfd3e60dc179963ef2ea58da5ec074fd3d9e7a1e7ba97f"},
|
||||
{file = "numpy-2.0.1.tar.gz", hash = "sha256:485b87235796410c3519a699cfe1faab097e509e90ebb05dcd098db2ae87e7b3"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "opencv-python"
|
||||
version = "4.10.0.84"
|
||||
description = "Wrapper package for OpenCV python bindings."
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
files = [
|
||||
{file = "opencv-python-4.10.0.84.tar.gz", hash = "sha256:72d234e4582e9658ffea8e9cae5b63d488ad06994ef12d81dc303b17472f3526"},
|
||||
{file = "opencv_python-4.10.0.84-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:fc182f8f4cda51b45f01c64e4cbedfc2f00aff799debebc305d8d0210c43f251"},
|
||||
{file = "opencv_python-4.10.0.84-cp37-abi3-macosx_12_0_x86_64.whl", hash = "sha256:71e575744f1d23f79741450254660442785f45a0797212852ee5199ef12eed98"},
|
||||
{file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09a332b50488e2dda866a6c5573ee192fe3583239fb26ff2f7f9ceb0bc119ea6"},
|
||||
{file = "opencv_python-4.10.0.84-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ace140fc6d647fbe1c692bcb2abce768973491222c067c131d80957c595b71f"},
|
||||
{file = "opencv_python-4.10.0.84-cp37-abi3-win32.whl", hash = "sha256:2db02bb7e50b703f0a2d50c50ced72e95c574e1e5a0bb35a8a86d0b35c98c236"},
|
||||
{file = "opencv_python-4.10.0.84-cp37-abi3-win_amd64.whl", hash = "sha256:32dbbd94c26f611dc5cc6979e6b7aa1f55a64d6b463cc1dcd3c95505a63e48fe"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""}
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = "^3.12"
|
||||
content-hash = "c6a88e50b9918828b07e6735e0b8bc6137d6e6cbb83f7f180a5cd140fdb2b20f"
|
@ -0,0 +1,20 @@
|
||||
[tool.poetry]
|
||||
name = "maze"
|
||||
version = "0.1.0"
|
||||
description = "An OpenCV project that detects a maze drawn on a whiteboard and solves it"
|
||||
authors = ["Antonio De Lucreziis <antonio.delucreziis@gmail.com>"]
|
||||
license = "MIT"
|
||||
readme = "README.md"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
python = "^3.12"
|
||||
dt-apriltags = "^3.1.7"
|
||||
numpy = "^2.0.1"
|
||||
apriltag = "^0.0.16"
|
||||
opencv-python = "^4.10.0.84"
|
||||
networkx = "^3.3"
|
||||
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core"]
|
||||
build-backend = "poetry.core.masonry.api"
|
@ -0,0 +1,118 @@
|
||||
import cv2
|
||||
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
|
||||
import dt_apriltags
|
||||
from dt_apriltags import Detector, Detection
|
||||
|
||||
|
||||
WINDOW_LABELS = set()
|
||||
|
||||
|
||||
def display_image(label, image, default_width: int = 800, default_height: int = 600, interpolation: int = cv2.INTER_LINEAR):
|
||||
"""
|
||||
Display an image fitted in a window with the given label and size, optionally setting the interpolation method
|
||||
"""
|
||||
|
||||
height, width = image.shape[0], image.shape[1]
|
||||
|
||||
if width < default_width or height < default_height:
|
||||
new_width = default_height * width // height
|
||||
image = cv2.resize(image, (new_width, default_height), interpolation=interpolation)
|
||||
|
||||
WINDOW_LABELS.add(label)
|
||||
cv2.namedWindow(label, cv2.WINDOW_NORMAL)
|
||||
cv2.resizeWindow(label, default_width, default_height)
|
||||
cv2.imshow(label, image)
|
||||
|
||||
|
||||
def wait_frame():
|
||||
"""
|
||||
Wait for the user to press any key
|
||||
"""
|
||||
|
||||
if cv2.waitKey(1) == ord("q"):
|
||||
cv2.destroyAllWindows()
|
||||
return False
|
||||
|
||||
if any(cv2.getWindowProperty(window, cv2.WND_PROP_VISIBLE) < 1 for window in WINDOW_LABELS):
|
||||
cv2.destroyAllWindows()
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def wait_close_app():
|
||||
"""
|
||||
Wait for the user to press the 'q' key or to close any of the windows
|
||||
"""
|
||||
|
||||
while True:
|
||||
if cv2.waitKey(1) == ord("q"):
|
||||
break
|
||||
|
||||
# check if any window of the windows is closed
|
||||
if any(cv2.getWindowProperty(window, cv2.WND_PROP_VISIBLE) < 1 for window in WINDOW_LABELS):
|
||||
break
|
||||
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
DEFAULT_DETECTOR = Detector(
|
||||
families="tag25h9",
|
||||
nthreads=2,
|
||||
quad_decimate=2.0,
|
||||
quad_sigma=0.8,
|
||||
refine_edges=1,
|
||||
decode_sharpening=0.25,
|
||||
)
|
||||
|
||||
|
||||
def extract_tags_dict(image, detector=DEFAULT_DETECTOR) -> dict[int, list[Detection]]:
|
||||
"""
|
||||
Extract tags from the given image using the given detector
|
||||
"""
|
||||
|
||||
tags: list[Detection] = detector.detect(image)
|
||||
|
||||
tag_registry: dict[int, list[Detection]] = dict()
|
||||
|
||||
for tag in tags:
|
||||
tag_id = tag.tag_id
|
||||
|
||||
if tag_id not in tag_registry:
|
||||
tag_registry[tag_id] = []
|
||||
|
||||
tag_registry[tag_id].append(tag)
|
||||
|
||||
return tag_registry
|
||||
|
||||
|
||||
def extract_tags_dict_single(image, detector=DEFAULT_DETECTOR) -> dict[int, Detection]:
|
||||
"""
|
||||
Extract tags from the given image using the given detector and return only the first tag found for each tag id
|
||||
"""
|
||||
|
||||
tag_registry = extract_tags_dict(image, detector)
|
||||
|
||||
return {tag_id: tags[0] for tag_id, tags in tag_registry.items()}
|
||||
|
||||
|
||||
def extract_foreground(image):
|
||||
"""
|
||||
Extracts the background from the given image and removes it, returning the normalized image
|
||||
"""
|
||||
|
||||
median_blur_size = 201
|
||||
|
||||
bg_img = cv2.medianBlur(image, median_blur_size)
|
||||
|
||||
# display_image("Background", bg_img)
|
||||
|
||||
diff_img = 255 - cv2.absdiff(image, bg_img)
|
||||
|
||||
norm_img = cv2.normalize(diff_img, None, alpha=0, beta=255, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_8UC1)
|
||||
|
||||
return norm_img
|
Loading…
Reference in New Issue