mirror of
https://github.com/SikongJueluo/Mini-Nav.git
synced 2026-07-12 20:15:31 +08:00
feat(scenegraph): add ImageHashPipeline protocol and update query API
- Introduce ImageHashPipeline Protocol for extensible hash computation - Rename query_crop_index to query_index for clarity - Make query_crop nullable to handle missing crop edge case - Add keyword-only arguments for better API clarity - Handle empty scene graph objects gracefully - Add comprehensive test coverage for query_image_against_scene_graph
This commit is contained in:
@@ -5,7 +5,6 @@ from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
from PIL import Image
|
||||
|
||||
@@ -88,11 +87,11 @@ def test_query_image_against_scene_graph_returns_exact_node_match():
|
||||
pipeline = FakePipeline(query_bits.unsqueeze(0), [crop])
|
||||
|
||||
results = query_image_against_scene_graph(
|
||||
_image(), pipeline, graph, ["a chair"], top_k=1, batch_size=7
|
||||
_image(), pipeline=pipeline, scene_graph=graph, text_labels=["a chair"], top_k=1, batch_size=7
|
||||
)
|
||||
|
||||
assert len(results) == 1
|
||||
assert results[0].query_crop_index == 0
|
||||
assert results[0].query_index == 0
|
||||
assert results[0].query_hash == query_hash
|
||||
assert results[0].query_crop is crop
|
||||
assert len(results[0].matches) == 1
|
||||
@@ -114,10 +113,10 @@ def test_query_image_against_scene_graph_returns_one_result_per_query_crop():
|
||||
pipeline = FakePipeline(torch.stack([bits_a, bits_b]), [crop_a, crop_b])
|
||||
|
||||
results = query_image_against_scene_graph(
|
||||
_image(), pipeline, graph, ["a chair"], top_k=1
|
||||
_image(), pipeline=pipeline, scene_graph=graph, text_labels=["a chair"], top_k=1
|
||||
)
|
||||
|
||||
assert [result.query_crop_index for result in results] == [0, 1]
|
||||
assert [result.query_index for result in results] == [0, 1]
|
||||
assert [result.query_hash for result in results] == [hash_a, hash_b]
|
||||
assert [result.query_crop for result in results] == [crop_a, crop_b]
|
||||
assert [result.matches[0].obj_id for result in results] == ["obj_a", "obj_b"]
|
||||
@@ -133,7 +132,7 @@ def test_query_image_against_scene_graph_preserves_topk_match_order():
|
||||
pipeline = FakePipeline(query_bits.unsqueeze(0), [_image("red")])
|
||||
|
||||
results = query_image_against_scene_graph(
|
||||
_image(), pipeline, graph, ["object"], top_k=3
|
||||
_image(), pipeline=pipeline, scene_graph=graph, text_labels=["object"], top_k=3
|
||||
)
|
||||
|
||||
assert [match.obj_id for match in results[0].matches] == [
|
||||
@@ -148,29 +147,71 @@ def test_query_image_against_scene_graph_returns_empty_list_for_no_hashes():
|
||||
pipeline = FakePipeline(torch.empty((0, WIDTH), dtype=torch.int32), [])
|
||||
|
||||
results = query_image_against_scene_graph(
|
||||
_image(), pipeline, SimpleSceneGraph(), ["object"]
|
||||
_image(), pipeline=pipeline, scene_graph=SimpleSceneGraph(), text_labels=["object"]
|
||||
)
|
||||
|
||||
assert results == []
|
||||
|
||||
|
||||
def test_query_image_against_scene_graph_rejects_hash_crop_count_mismatch():
|
||||
pipeline = FakePipeline(torch.stack([_bits_with_ones(0), _bits_with_ones(1)]), [_image()])
|
||||
def test_missing_crop_returns_none_and_extra_crop_ignored():
|
||||
"""Spec: missing crop index yields None, extra cropped images are ignored."""
|
||||
first_bits = _bits_with_ones(0)
|
||||
second_bits = _bits_with_ones(1)
|
||||
first_hash = bits_tensor_to_hash_bytes(first_bits)
|
||||
second_hash = bits_tensor_to_hash_bytes(second_bits)
|
||||
only_crop = _image("blue")
|
||||
graph = _scene_graph_with_hashes(
|
||||
("first", first_hash), ("second", second_hash)
|
||||
)
|
||||
|
||||
with pytest.raises(ValueError, match="hash_bits and cropped_images must align"):
|
||||
query_image_against_scene_graph(
|
||||
_image(), pipeline, SimpleSceneGraph(), ["object"]
|
||||
)
|
||||
# Fewer cropped_images than hash_bits rows -> missing crop becomes None
|
||||
missing_pipeline = FakePipeline(
|
||||
torch.stack([first_bits, second_bits]), [only_crop]
|
||||
)
|
||||
results = query_image_against_scene_graph(
|
||||
_image("red"),
|
||||
pipeline=missing_pipeline,
|
||||
scene_graph=graph,
|
||||
text_labels=["object"],
|
||||
)
|
||||
assert [result.query_crop for result in results] == [only_crop, None]
|
||||
|
||||
# More cropped_images than hash_bits rows -> extra crop is ignored
|
||||
extra_pipeline = FakePipeline(
|
||||
first_bits.unsqueeze(0),
|
||||
[only_crop, _image("yellow")],
|
||||
)
|
||||
extra_results = query_image_against_scene_graph(
|
||||
_image("red"),
|
||||
pipeline=extra_pipeline,
|
||||
scene_graph=graph,
|
||||
text_labels=["object"],
|
||||
)
|
||||
assert len(extra_results) == 1
|
||||
assert extra_results[0].query_crop is only_crop
|
||||
|
||||
|
||||
def test_scenegraph_package_exports_image_query_api():
|
||||
from scenegraph import ( # noqa: PLC0415
|
||||
ImageHashPipeline,
|
||||
ImageSceneGraphQueryResult,
|
||||
query_image_against_scene_graph as exported_query_image_against_scene_graph,
|
||||
)
|
||||
from scenegraph.query import ( # noqa: PLC0415
|
||||
ImageHashPipeline as DirectImageHashPipeline,
|
||||
ImageSceneGraphQueryResult as DirectImageSceneGraphQueryResult,
|
||||
query_image_against_scene_graph as direct_query,
|
||||
)
|
||||
|
||||
assert ImageHashPipeline is DirectImageHashPipeline
|
||||
assert ImageSceneGraphQueryResult is DirectImageSceneGraphQueryResult
|
||||
assert exported_query_image_against_scene_graph is query_image_against_scene_graph
|
||||
assert exported_query_image_against_scene_graph is direct_query
|
||||
|
||||
import scenegraph # noqa: PLC0415
|
||||
|
||||
required = {
|
||||
"ImageHashPipeline",
|
||||
"ImageSceneGraphQueryResult",
|
||||
"query_image_against_scene_graph",
|
||||
}
|
||||
assert required <= set(scenegraph.__all__)
|
||||
|
||||
Reference in New Issue
Block a user