diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f18f725 --- /dev/null +++ b/.gitignore @@ -0,0 +1,178 @@ +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# image data +*.png +*.jpeg + +# output +*.csv \ No newline at end of file diff --git a/images/.gitkeep b/images/.gitkeep new file mode 100644 index 0000000..e69de29 --- /dev/null +++ b/images/.gitkeep diff --git a/main.py b/main.py new file mode 100644 index 0000000..fd8d832 --- /dev/null +++ b/main.py @@ -0,0 +1,198 @@ +import cv2 +import numpy as np +import os +from dotenv import load_dotenv +from pathlib import Path +import csv +import re + +# .envファイルから環境変数を読み込む +load_dotenv() + +# 画像ディレクトリのパスを取得 +IMAGE_DIR = os.getenv("IMAGE_DIR") + +# グローバル変数 +clicked_points = [] +detected_centers = [] +detected_areas = [] +current_image = None +current_image_name = "" +all_results = [] # すべての画像の結果を保存するリスト + + +def region_growing(image, seed_point, threshold=60, max_distance=50): + height, width = image.shape[:2] + segmented = np.zeros((height, width), dtype=np.uint8) + + # 8近傍 + neighbors = [(0, 1), (1, 0), (0, -1), (-1, 0), (1, 1), (-1, -1), (1, -1), (-1, 1)] + + stack = [seed_point] + segmented[seed_point[1], seed_point[0]] = 255 + seed_color = image[seed_point[1], seed_point[0]] + + while stack: + x, y = stack.pop() + for dx, dy in neighbors: + nx, ny = x + dx, y + dy + if 0 <= nx < width and 0 <= ny < height: + if segmented[ny, nx] == 0: + # シード点からの距離を計算 + distance = np.sqrt((nx - seed_point[0]) ** 2 + (ny - seed_point[1]) ** 2) + if distance <= max_distance: + pixel_color = image[ny, nx] + if np.all(np.abs(seed_color.astype(int) - pixel_color.astype(int)) <= threshold): + segmented[ny, nx] = 255 + stack.append((nx, ny)) + + return segmented + + +def detect_ping_pong_ball(image, click_point, max_distance): + x, y = click_point + hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) + + # 領域拡張法で似た色の領域を抽出(範囲制限付き) + segmented = region_growing(hsv, (x, y), threshold=50, max_distance=max_distance) + + # ノイズ除去 + kernel = np.ones((5, 5), np.uint8) + segmented = cv2.morphologyEx(segmented, cv2.MORPH_OPEN, kernel) + segmented = cv2.morphologyEx(segmented, cv2.MORPH_CLOSE, kernel) + + # 重心計算 + M = cv2.moments(segmented) + if M["m00"] > 0: + cX = int(M["m10"] / M["m00"]) + cY = int(M["m01"] / M["m00"]) + area = cv2.countNonZero(segmented) + print(f"Region detected: center=({cX}, {cY}), area={area}") + return (cX, cY), area + + print(f"No region detected. Returning clicked point: {click_point}") + return click_point, 0 + + +def on_mouse_click(event, x, y, flags, param): + global current_image, clicked_points, detected_centers, detected_areas + if event == cv2.EVENT_LBUTTONDOWN: + clicked_points.append((x, y)) + print(f"Clicked point: ({x}, {y})") + center, area = detect_ping_pong_ball(current_image, (x, y), max_distance=10) + detected_centers.append(center) + detected_areas.append(area) + + if center == (x, y): + print("Warning: Detected center is the same as clicked point.") + else: + print(f"Detected center differs from clicked point. Offset: ({center[0]-x}, {center[1]-y})") + + # 検出結果を描画 + draw_detection() + elif event == cv2.EVENT_RBUTTONDOWN: + if clicked_points: + clicked_points.pop() + detected_centers.pop() + detected_areas.pop() + print("Last click undone") + draw_detection() + + +def draw_detection(): + global current_image, clicked_points, detected_centers, detected_areas + image_copy = current_image.copy() + for click, center, area in zip(clicked_points, detected_centers, detected_areas): + cv2.circle(image_copy, center, int(np.sqrt(area / np.pi)), (0, 255, 0), 2) + cv2.circle(image_copy, center, 2, (0, 0, 255), 3) + cv2.circle(image_copy, click, 2, (255, 0, 0), -1) # クリックした点を青で表示 + cv2.imshow("Image", image_copy) + + +def start_detection(): + global clicked_points, detected_centers, detected_areas + clicked_points = [] + detected_centers = [] + detected_areas = [] + cv2.setMouseCallback("Image", on_mouse_click) + print("Detection started. Click on ping pong balls. Right-click to undo last click.") + + +def save_results(): + global clicked_points, detected_centers, current_image_name, detected_areas, all_results + result = [current_image_name] + for click, center in zip(clicked_points, detected_centers): + result.extend([click[0], click[1], center[0], center[1]]) + all_results.append(result) + print(f"Results for {current_image_name} added to all_results") + print("Detected centers:") + for center in detected_centers: + print(center) + + +def save_all_results(): + global all_results + with open("ping-pong-detection.csv", "w", newline="") as csvfile: + writer = csv.writer(csvfile) + header = ["image_file_name"] + for i in range(1, (len(max(all_results, key=len)) - 1) // 4 + 1): + header.extend([f"Pos{i}_click_x", f"Pos{i}_click_y", f"Pos{i}_center_x", f"Pos{i}_center_y"]) + writer.writerow(header) + for result in all_results: + writer.writerow(result) + print("All results saved to ping-pong-detection.csv") + + +def sort_key(filename): + # ファイル名からangleの前の数値を抽出 + match = re.search(r"_(-?\d+)_angle", filename) + if match: + return int(match.group(1)) + return 0 # マッチしない場合は0を返す + + +def main(): + global current_image, current_image_name + if not IMAGE_DIR: + print("Error: IMAGE_DIR is not set in .env file") + return + + image_dir = Path(IMAGE_DIR) + if not image_dir.is_dir(): + print(f"Error: {IMAGE_DIR} is not a valid directory") + return + + # 画像ファイルを取得し、angleの前の数値でソート + image_files = list(image_dir.glob("*.png")) + list(image_dir.glob("*.jpg")) + image_files.sort(key=lambda x: sort_key(x.name)) + + cv2.namedWindow("Image") + + for image_file in image_files: + current_image = cv2.imread(str(image_file)) + current_image_name = image_file.stem + if current_image is None: + print(f"Error: Unable to read image {image_file}") + continue + + cv2.imshow("Image", current_image) + print(f"Current image: {image_file.name}") + + start_detection() + + while True: + key = cv2.waitKey(1) & 0xFF + if key == ord("f"): # 'f' key to finish and save results + save_results() + break + elif key == 27: # ESC key to exit + cv2.destroyAllWindows() + save_all_results() + return + + cv2.destroyAllWindows() + save_all_results() + + +if __name__ == "__main__": + main() diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7539e4b --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +opencv-python +python-dotenv \ No newline at end of file