ブログ書くのめっちゃ久々!まぁ気が向いたときに書こうというブログなので、これで良いのだぁ
タイトルの通り、Pythonを使っててCみたいなハマり方をしました((+_+)) Pythonを使ってだいぶ経ちますが、このハマり方は初めてでした。
最近、ChatGPTにデバッグやコードの整理をしてもらうのですが、今回の問題はChatGPTさんも何が問題かわからなかったみたいです。
Contents
ハマったコード
ハマったコードは以下です。インターフェースに使っているコードは関係ないので省略します。何が問題かわかりますか?
コードは、Open3Dに実装されているKDTreeを使って、与えられた点(クエリ)の最近傍点をデータから探すコードです。KDTreeを自分で工夫してみたくて、自分の実装との速度比較用にOpen3Dのものを使って実装したものです。
一見すると問題なく、ChatGPTも問題を特定できず、実行してもエラー無く動きます。しかし、返ってくる結果が最近傍点を返したり、返さなかったりと、変な挙動をします。
このコードの何が問題かわかりますか?
# open3dの実装を使用したkd-treeの実装
from __future__ import annotations
import numpy as np
import open3d as o3d
from kdtree_lib.interface import KDTree
class KDTreeOpen3d(KDTree):
"""Open3DのKDTreeの実装"""
def __init__(self, points: np.ndarray) -> None:
"""コンストラクタ"""
if not isinstance(points, np.ndarray):
raise TypeError("pointsはNumPy配列である必要があります。")
self.points: np.ndarray = points
self.tree : o3d.geometry.KDTreeFlann = self.build_tree(self.points)
def build_tree(self, points: np.ndarray, depth: int = 0) -> o3d.geometry.KDTreeFlann:
"""KDTreeを構築する"""
pcd = o3d.geometry.PointCloud()
pcd.points = o3d.utility.Vector3dVector(points)
return o3d.geometry.KDTreeFlann(pcd)
def _nearest_neighbor_impl(self, point: np.ndarray | list | tuple, depth: int = 0) -> tuple[float, np.ndarray | None]:
"""最近傍点を探索する"""
[k, idx, _] = self.tree.search_knn_vector_3d(point, 1)
print(k, idx, _)
nearest_point = self.points[idx[0]]
dist = float(np.linalg.norm(np.array(point) - nearest_point))
return dist, nearest_point
if __name__ == "__main__":
# 3次元データ点の例
points = np.array([(2, 3, 1), (5, 4, 2), (9, 6, 3), (4, 7, 5), (8, 1, 8), (7, 2, 9)])
tree = KDTreeOpen3d(points)
# クエリポイント(3次元)
query_point = np.array((2.5, 3.5, 1.5))
distance, nearest_point = tree.nearest_neighbor(query_point)
print(f"Nearest to {query_point} is {nearest_point} with distance {distance}")
このコードの問題点
build_treeメソッド内の変数pcdが原因でした。
pcdはbuild_treeメソッド内のローカル変数なので、build_treeの実行が終わったら__del__によって開放されます。しかし、Open3DのKDTreeFlannで作ったKDTreeのデータの実態はpcdです。
そのため、build_treeの後で実行される近傍探索の実行時には、self.tree内のデータが保証されません。状況によってpcdのデータが記録されていたメモリ上の値が勝手にいじられて、うまくいったりいかなかったりします。
Cを触ってたときのような挙動をPythonで踏むのは初めてでビックリです( ;∀;) まぁ、pybindされてるC++のコードを使ってるわけだからそれはそうなんですが。
この問題の対策
変数pcdがbuild_tree内のローカル変数になってるから、もっと長く生存するようにすれば対策できます。そのため、クラスのインスタンス変数としてself.pcdにすると上手くいきます。
treeがpcdを参照してるってことをPython側に伝えるのってできないのかな。。。
以上です(*’ω’*)
No responses yet