Files
2025-01-28 22:40:08 +01:00

393 lines
12 KiB
Python

import json
import time
from pathlib import Path
from unittest.mock import Mock, patch
import numpy as np
import pytest
import torch
from PIL import Image
from node import DinoxDetectorNode, DinoxRegionVLNode
@pytest.fixture
def mock_client():
with patch("node.Client") as mock:
client_instance = Mock()
mock.return_value = client_instance
yield client_instance
@pytest.fixture
def mock_config():
with patch("node.Config") as mock:
config_instance = Mock()
mock.return_value = config_instance
yield config_instance
@pytest.fixture
def synthetic_image():
"""Create a synthetic test image tensor [B,H,W,C]"""
img = np.zeros((100, 100, 3), dtype=np.float32)
img[30:70, 30:70] = 1.0 # White square
# Convert to torch tensor with batch dimension
return torch.from_numpy(img).unsqueeze(0) # [1,100,100,3]
@pytest.fixture
def real_image():
"""Load real test image and convert to tensor [B,H,W,C]"""
img = Image.open("test_leather_jacket.jpg")
img_np = np.array(img).astype(np.float32) / 255.0
return torch.from_numpy(img_np).unsqueeze(0) # Add batch dimension
@pytest.fixture
def batched_image():
"""Create a batched test image tensor [B,H,W,C]"""
img = np.zeros((1, 100, 100, 3), dtype=np.float32)
img[0, 30:70, 30:70] = 1.0 # White square in first image
return torch.from_numpy(img)
@pytest.fixture
def mock_predictions():
return create_mock_predictions(100, 100)
@pytest.fixture
def mock_predictions_with_size(test_image):
h, w = test_image.shape[1:3] # Get H,W from tensor [B,H,W,C]
return create_mock_predictions(h, w)
def create_mock_predictions(height, width):
prediction = Mock()
prediction.bbox = [30, 30, 70, 70]
prediction.mask = Mock()
prediction.mask.counts = "test_rle_counts"
prediction.mask.size = [height, width]
prediction.score = 0.95
prediction.category = "test_object"
prediction.pose = [[x, y, 1.0] for x, y in zip(range(10), range(10))]
prediction.hand = [[x, y, 1.0] for x, y in zip(range(5), range(5))]
return [prediction]
@pytest.fixture(params=["synthetic", "real", "batched"])
def test_image(request, synthetic_image, real_image, batched_image):
if request.param == "synthetic":
return synthetic_image
elif request.param == "real":
return real_image
else:
return batched_image
def test_detector_input_types():
node = DinoxDetectorNode()
input_types = node.INPUT_TYPES()
assert "required" in input_types
assert "image" in input_types["required"]
assert "text_prompt" in input_types["required"]
assert "api_token" in input_types["required"]
assert "bbox_threshold" in input_types["required"]
assert "iou_threshold" in input_types["required"]
assert "prompt_type" in input_types["required"]
assert "return_pose" in input_types["required"]
assert "return_hand" in input_types["required"]
def test_detector_return_types():
node = DinoxDetectorNode()
assert node.RETURN_TYPES == ("IMAGE", "MASK", "STRING", "STRING")
assert node.RETURN_NAMES == (
"box_annotated",
"binary_mask",
"pose_data",
"hand_data",
)
assert node.CATEGORY == "detection"
def test_output_directory_creation():
node = DinoxDetectorNode()
assert Path("./outputs/dinox").exists()
@patch("node.DetectionTask")
def test_detect_and_annotate(
mock_detection_task,
mock_client,
mock_config,
test_image,
mock_predictions_with_size,
):
node = DinoxDetectorNode()
# Mock API response
mock_client.upload_file.return_value = "mock_url"
task_instance = Mock()
task_instance.result = Mock()
task_instance.result.objects = mock_predictions_with_size
mock_detection_task.return_value = task_instance
# Mock RLE to mask conversion
with patch("node.DetectionTask.rle2mask") as mock_rle2mask:
with patch("node.DetectionTask.string2rle") as mock_string2rle:
mock_rle2mask.return_value = np.ones(
(
mock_predictions_with_size[0].mask.size[0],
mock_predictions_with_size[0].mask.size[1],
),
dtype=bool,
)
mock_string2rle.return_value = "decoded_rle"
# Run detection with all features
box_result, mask_result, pose_data, hand_data = node.detect_and_annotate(
test_image,
"test_object",
"test_token",
0.25,
0.8,
prompt_type="text",
return_pose=True,
return_hand=True,
)
# Verify tensor outputs
assert isinstance(box_result, torch.Tensor)
assert isinstance(mask_result, torch.Tensor)
assert len(box_result.shape) == 4 # [B,H,W,C]
assert len(mask_result.shape) == 3 # [B,H,W]
assert box_result.shape[0] == 1 # Batch size 1
assert mask_result.shape[0] == 1 # Batch size 1
assert box_result.shape[1:3] == test_image.shape[1:3] # Same H,W as input
assert mask_result.shape[1:] == test_image.shape[1:3] # Same H,W as input
assert box_result.shape[-1] == 3 # RGB channels
assert torch.all(box_result >= 0) and torch.all(box_result <= 1) # Values in [0,1]
assert torch.all(mask_result >= 0) and torch.all(
mask_result <= 1
) # Values in [0,1]
# Verify pose and hand data are valid JSON strings
assert isinstance(pose_data, str)
assert isinstance(hand_data, str)
pose_json = json.loads(pose_data)
hand_json = json.loads(hand_data)
assert isinstance(pose_json, list)
assert isinstance(hand_json, list)
if pose_json: # If not empty
assert all(len(point) == 3 for point in pose_json) # x,y,confidence
if hand_json: # If not empty
assert all(len(point) == 3 for point in hand_json) # x,y,confidence
def test_leather_jacket_detection(real_image):
node = DinoxDetectorNode()
# Use real API with provided token
try:
box_result, mask_result, pose_data, hand_data = node.detect_and_annotate(
real_image,
"leather jacket . person",
"73b1fca55fffdf29a7f777d965bb64cf",
0.25,
0.8,
prompt_type="text",
return_pose=True,
return_hand=True,
)
# Verify tensor outputs
assert isinstance(box_result, torch.Tensor)
assert isinstance(mask_result, torch.Tensor)
assert len(box_result.shape) == 4 # [B,H,W,C]
assert len(mask_result.shape) == 3 # [B,H,W]
assert box_result.shape[0] == 1 # Batch size 1
assert mask_result.shape[0] == 1 # Batch size 1
assert box_result.shape[1:3] == real_image.shape[1:3] # Same H,W as input
assert mask_result.shape[1:] == real_image.shape[1:3] # Same H,W as input
assert box_result.shape[-1] == 3 # RGB channels
# Verify pose and hand data are valid JSON strings
assert isinstance(pose_data, str)
assert isinstance(hand_data, str)
json.loads(pose_data) # Should not raise error
json.loads(hand_data) # Should not raise error
# Save outputs for visual inspection
Path("outputs").mkdir(parents=True, exist_ok=True)
Image.fromarray((box_result[0].numpy() * 255).astype(np.uint8)).save(
"outputs/test_leather_jacket_box.jpg"
)
Image.fromarray((mask_result[0].numpy() * 255).astype(np.uint8)).save(
"outputs/test_leather_jacket_mask.png"
)
# Save keypoints if available
if pose_data != "[]":
with open("outputs/test_leather_jacket_pose.json", "w") as f:
f.write(pose_data)
if hand_data != "[]":
with open("outputs/test_leather_jacket_hand.json", "w") as f:
f.write(hand_data)
except Exception as e:
print(f"API test failed with error: {str(e)}")
assert False, f"API test failed: {str(e)}"
def test_region_analysis(real_image):
node = DinoxRegionVLNode()
# Test regions
regions = [[30, 30, 70, 70], [50, 50, 90, 90]]
try:
captions, roc_results, ocr_results = node.analyze_regions(
real_image,
json.dumps(regions),
"73b1fca55fffdf29a7f777d965bb64cf",
prompt_type="text",
text_prompt="leather jacket . person",
return_caption=True,
return_roc=True,
return_ocr=True,
)
# Verify results are valid JSON strings
assert isinstance(captions, str)
assert isinstance(roc_results, str)
assert isinstance(ocr_results, str)
# Verify JSON format
captions_dict = json.loads(captions)
roc_dict = json.loads(roc_results)
ocr_dict = json.loads(ocr_results)
assert isinstance(captions_dict, dict)
assert isinstance(roc_dict, dict)
assert isinstance(ocr_dict, dict)
# Save results for inspection
Path("outputs").mkdir(parents=True, exist_ok=True)
with open("outputs/test_region_analysis.json", "w") as f:
json.dump(
{"captions": captions_dict, "roc": roc_dict, "ocr": ocr_dict},
f,
indent=2,
)
except Exception as e:
print(f"Region analysis failed with error: {str(e)}")
assert False, f"Region analysis failed: {str(e)}"
@pytest.mark.parametrize("threshold", [-0.1, 1.1])
def test_invalid_threshold(threshold, test_image):
node = DinoxDetectorNode()
with pytest.raises(ValueError):
node.detect_and_annotate(
test_image, "test_object", "test_token", threshold, 0.8, prompt_type="text"
)
def test_empty_api_token(test_image):
node = DinoxDetectorNode()
with pytest.raises(ValueError):
node.detect_and_annotate(
test_image, "test_object", "", 0.25, 0.8, prompt_type="text"
)
def test_empty_text_prompt(test_image):
node = DinoxDetectorNode()
with pytest.raises(ValueError):
node.detect_and_annotate(
test_image, "", "test_token", 0.25, 0.8, prompt_type="text"
)
def test_invalid_image_type():
node = DinoxDetectorNode()
invalid_image = "not_an_image"
with pytest.raises(ValueError):
node.detect_and_annotate(
invalid_image, "test_object", "test_token", 0.25, 0.8, prompt_type="text"
)
def test_cleanup_temp_files(test_image):
node = DinoxDetectorNode()
temp_path = Path("./outputs/dinox/temp_input.jpg")
# Ensure clean state
if temp_path.exists():
try:
temp_path.unlink()
except Exception as e:
print(f"\nFailed to clean up existing temp file: {str(e)}")
try:
# Use real API with provided token
box_result, mask_result, pose_data, hand_data = node.detect_and_annotate(
test_image,
"test object",
"73b1fca55fffdf29a7f777d965bb64cf",
0.25,
0.8,
prompt_type="text",
return_pose=True,
return_hand=True,
)
# Verify outputs
assert isinstance(box_result, torch.Tensor)
assert isinstance(mask_result, torch.Tensor)
assert isinstance(pose_data, str)
assert isinstance(hand_data, str)
assert len(box_result.shape) == 4 # [B,H,W,C]
assert len(mask_result.shape) == 3 # [B,H,W]
# Verify JSON data
json.loads(pose_data) # Should not raise error
json.loads(hand_data) # Should not raise error
# Give the system a moment to complete file operations
time.sleep(0.5)
# Force Python to release file handles
import gc
gc.collect()
# Check if file exists and get info
if temp_path.exists():
print(f"\nTemp file still exists at: {temp_path}")
print(f"File size: {temp_path.stat().st_size} bytes")
print(f"File permissions: {oct(temp_path.stat().st_mode)}")
# Verify temp file is cleaned up
assert not temp_path.exists(), "Temporary file was not cleaned up"
except Exception as e:
print(f"\nTest failed with error: {str(e)}")
raise
finally:
# Clean up test outputs
for f in Path("outputs").glob("test_*"):
try:
f.unlink(missing_ok=True)
except Exception as e:
print(f"\nFailed to clean up test output {f}: {str(e)}")