Merge pull request #14 from Limbicnation/chore/lint-and-docs
Chore/lint and docs
This commit is contained in:
@@ -172,3 +172,9 @@ cython_debug/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
# IDE / Editor artifacts
|
||||
.claude/
|
||||
|
||||
# Build artifacts
|
||||
*.zip
|
||||
|
||||
@@ -69,8 +69,11 @@
|
||||
- `torch` - PyTorch for tensor operations
|
||||
- `numpy` - Numerical computing
|
||||
- `Pillow` - Image processing
|
||||
- `opencv-python` - Computer vision operations
|
||||
- `opencv-python` / `opencv-contrib-python` - Computer vision operations
|
||||
- `scikit-learn` - Machine learning algorithms for clustering
|
||||
- `ultralytics` - YOLO object detection for Auto GrabCut
|
||||
- `pydantic` - Parameter validation and sanitisation
|
||||
- `structlog` - Structured logging
|
||||
|
||||
### ⚠️ Important: Dependency Installation
|
||||
|
||||
@@ -372,6 +375,76 @@ The system automatically adjusts `edge_blur_amount` based on image analysis:
|
||||
|
||||
---
|
||||
|
||||
## 🖼️ Remove Background and Resize Node
|
||||
|
||||
### Overview
|
||||
The **RemoveBackgroundAndResizeNode** provides standalone PIL-only background removal with integrated resize functionality. Unlike the main transparency node, this implementation uses pure Pillow operations without OpenCV, making it lightweight and dependency-minimal for simple workflows.
|
||||
|
||||
### Key Features
|
||||
- **PIL-Only Processing**: No OpenCV required — uses Pillow for all image operations
|
||||
- **Integrated Resize**: Background removal and resizing in a single node
|
||||
- **Lightweight**: Minimal memory footprint for batch workflows
|
||||
|
||||
### Node Parameters
|
||||
|
||||
| Parameter | Type | Range/Options | Default | Description |
|
||||
|-----------|------|---------------|---------|-------------|
|
||||
| **width** | INT | 64-4096 | 512 | Output width |
|
||||
| **height** | INT | 64-4096 | 512 | Output height |
|
||||
| **upscale_method** | DROPDOWN | NEAREST, BILINEAR, BICUBIC, LANCZOS | LANCZOS | Interpolation method |
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Development
|
||||
|
||||
### Testing
|
||||
|
||||
```bash
|
||||
# Install test dependencies
|
||||
pip install pytest
|
||||
|
||||
# Run test suite
|
||||
python -m pytest tests/ -v
|
||||
|
||||
# Run standalone test scripts
|
||||
python test_scaling.py
|
||||
python test_power_of_8_scaling.py
|
||||
python test_standalone.py
|
||||
python test_power_of_8_standalone.py
|
||||
```
|
||||
|
||||
### Linting & Security
|
||||
|
||||
```bash
|
||||
# Run ruff (fast Python linter)
|
||||
ruff check .
|
||||
|
||||
# Run flake8 (critical error checks)
|
||||
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
|
||||
|
||||
# Run bandit (security scan)
|
||||
bandit -r .
|
||||
```
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
.
|
||||
├── __init__.py # ComfyUI node registration
|
||||
├── nodes.py # Core TransparencyBackgroundRemover nodes
|
||||
├── background_remover.py # EnhancedPixelArtProcessor engine
|
||||
├── grabcut_nodes.py # Auto GrabCut + Refinement nodes
|
||||
├── grabcut_remover.py # GrabCutProcessor engine
|
||||
├── RemoveBackgroundAndResizeNode.py # PIL-only resize node
|
||||
├── src/validation.py # Pydantic parameter validation
|
||||
├── tests/ # Pytest test suite
|
||||
├── requirements.txt # Python dependencies
|
||||
├── install.py # Dependency installer
|
||||
└── examples/ # Example images and workflows
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🐛 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
@@ -15,7 +15,7 @@ License: Apache-2.0
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Optional, Tuple
|
||||
from typing import Tuple
|
||||
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
# Check for optional dependencies and provide helpful warnings
|
||||
try:
|
||||
import sklearn
|
||||
import sklearn # noqa: F401
|
||||
SKLEARN_AVAILABLE = True
|
||||
except ImportError:
|
||||
SKLEARN_AVAILABLE = False
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
import colorsys
|
||||
from typing import Tuple, Optional, List
|
||||
|
||||
# Lazy import sklearn with graceful fallback
|
||||
|
||||
+9
-8
@@ -1,6 +1,5 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import time
|
||||
from typing import Optional, Tuple
|
||||
|
||||
import numpy as np
|
||||
@@ -10,10 +9,17 @@ from PIL import Image
|
||||
|
||||
log = structlog.get_logger(__name__)
|
||||
|
||||
import os.path as _path # noqa: E402
|
||||
import sys as _sys # noqa: E402
|
||||
|
||||
_parent = _path.dirname(__file__)
|
||||
if _parent not in _sys.path:
|
||||
_sys.path.insert(0, _parent)
|
||||
|
||||
# Try to import ComfyUI modules
|
||||
try:
|
||||
import folder_paths
|
||||
import comfy.utils
|
||||
import folder_paths # noqa: F401
|
||||
import comfy.utils # noqa: F401
|
||||
COMFY_AVAILABLE = True
|
||||
except ImportError:
|
||||
COMFY_AVAILABLE = False
|
||||
@@ -26,11 +32,6 @@ except ImportError:
|
||||
from grabcut_remover import GrabCutProcessor, create_fallback_processor, _log_gpu_memory
|
||||
|
||||
# Pydantic validation — security-hardened parameter sanitisation before GPU execution
|
||||
import os.path as _path
|
||||
import sys as _sys
|
||||
_parent = _path.dirname(__file__)
|
||||
if _parent not in _sys.path:
|
||||
_sys.path.insert(0, _parent)
|
||||
try:
|
||||
from src.validation import validate_node_params, GrabCutParams, MaskParams
|
||||
except ImportError:
|
||||
|
||||
+1
-1
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
import os
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import os
|
||||
import numpy as np
|
||||
import torch
|
||||
from PIL import Image
|
||||
import cv2
|
||||
import colorsys
|
||||
import tempfile
|
||||
|
||||
# Handle ComfyUI imports gracefully for testing
|
||||
try:
|
||||
@@ -17,7 +16,7 @@ except ImportError:
|
||||
|
||||
# Create mock folder_paths
|
||||
folder_paths = ModuleType('folder_paths')
|
||||
folder_paths.get_input_directory = lambda: '/tmp'
|
||||
folder_paths.get_input_directory = lambda: tempfile.gettempdir()
|
||||
sys.modules['folder_paths'] = folder_paths
|
||||
|
||||
# Create mock comfy.utils
|
||||
|
||||
+4
-6
@@ -6,9 +6,7 @@ Tests both standalone processing and ComfyUI node integration.
|
||||
|
||||
import numpy as np
|
||||
import cv2
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import time
|
||||
|
||||
# Test imports
|
||||
print("Testing imports...")
|
||||
@@ -78,7 +76,7 @@ def test_grabcut_processor():
|
||||
result = processor.process_with_grabcut(test_img, target_class="auto")
|
||||
|
||||
if result['success']:
|
||||
print(f"✓ Processing successful!")
|
||||
print("✓ Processing successful!")
|
||||
print(f" - Processing time: {result['processing_time_ms']}ms")
|
||||
print(f" - Detection confidence: {result['confidence']:.2f}")
|
||||
if result['bbox']:
|
||||
@@ -334,7 +332,7 @@ def test_bbox_validation_fixes():
|
||||
print(f"✗ Error with safety margin {safety_margin}: {e}")
|
||||
|
||||
# Test minimum bbox size functionality with small object simulation
|
||||
print(f"\n--- Testing min_bbox_size functionality ---")
|
||||
print("\n--- Testing min_bbox_size functionality ---")
|
||||
|
||||
# Create a small test image with a tiny object to force min_bbox_size logic
|
||||
small_test_image = torch.zeros((1, 200, 200, 3)) # Small 200x200 image
|
||||
@@ -373,7 +371,7 @@ def test_bbox_validation_fixes():
|
||||
else:
|
||||
print(f" ✗ Bbox size not enforced: {actual_width}x{actual_height} < {min_size}")
|
||||
else:
|
||||
print(f" ⚠ No valid bbox detected, using fallback")
|
||||
print(" ⚠ No valid bbox detected, using fallback")
|
||||
|
||||
except Exception as e:
|
||||
print(f" ✗ Error with min_bbox_size {min_size}: {e}")
|
||||
@@ -431,7 +429,7 @@ def test_edge_blur_functionality():
|
||||
if len(mask_values) > 2:
|
||||
print(f" ✓ Soft edges detected (unique values: {len(mask_values)})")
|
||||
else:
|
||||
print(f" ⚠ Expected soft edges but found binary mask")
|
||||
print(" ⚠ Expected soft edges but found binary mask")
|
||||
else:
|
||||
print(f" ✓ Binary edges preserved (unique values: {len(mask_values)})")
|
||||
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@ from grabcut_nodes import NODE_CLASS_MAPPINGS as GRABCUT_MAPPINGS, NODE_DISPLAY_
|
||||
NODE_CLASS_MAPPINGS = {**NODES_MAPPINGS, **GRABCUT_MAPPINGS}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {**NODES_DISPLAY_MAPPINGS, **GRABCUT_DISPLAY_MAPPINGS}
|
||||
|
||||
print(f"✓ Successfully imported and combined node mappings")
|
||||
print("✓ Successfully imported and combined node mappings")
|
||||
print(f"Found {len(NODE_CLASS_MAPPINGS)} total nodes:")
|
||||
for name, cls in NODE_CLASS_MAPPINGS.items():
|
||||
display = NODE_DISPLAY_NAME_MAPPINGS.get(name, "Unknown")
|
||||
|
||||
@@ -97,7 +97,7 @@ def test_input_types():
|
||||
if sizes == expected_sizes:
|
||||
print("✅ output_size parameter configured correctly")
|
||||
else:
|
||||
print(f"❌ output_size sizes mismatch")
|
||||
print("❌ output_size sizes mismatch")
|
||||
print(f" Expected: {expected_sizes}")
|
||||
print(f" Got: {sizes}")
|
||||
return False
|
||||
|
||||
@@ -115,7 +115,7 @@ def test_input_types():
|
||||
if sizes == expected_sizes:
|
||||
print("✅ output_size parameter configured correctly")
|
||||
else:
|
||||
print(f"❌ output_size sizes mismatch")
|
||||
print("❌ output_size sizes mismatch")
|
||||
print(f" Expected: {expected_sizes}")
|
||||
print(f" Got: {sizes}")
|
||||
return False
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import numpy as np
|
||||
import pytest
|
||||
import torch
|
||||
|
||||
|
||||
|
||||
@@ -109,7 +109,6 @@ class TestRefineEdges:
|
||||
|
||||
def test_refine_edges_returns_uint8_mask(self, grabcut_processor, sample_mask_hw):
|
||||
"""refine_edges returns uint8 array with 0/255 values."""
|
||||
import cv2
|
||||
rgb = np.zeros((256, 256, 3), dtype=np.uint8)
|
||||
result = grabcut_processor.refine_edges(sample_mask_hw, rgb)
|
||||
assert result.dtype == np.uint8
|
||||
@@ -121,7 +120,6 @@ class TestRefineEdges:
|
||||
"""With zero refinement strength, mask is returned unchanged (type may differ)."""
|
||||
grabcut_processor.edge_refinement_strength = 0.0
|
||||
grabcut_processor.edge_blur_amount = 0.0
|
||||
import cv2
|
||||
rgb = np.zeros((256, 256, 3), dtype=np.uint8)
|
||||
result = grabcut_processor.refine_edges(sample_mask_hw, rgb)
|
||||
assert result.shape == sample_mask_hw.shape
|
||||
|
||||
Reference in New Issue
Block a user