Adding image loading

This commit is contained in:
2025-12-08 16:28:58 +02:00
parent 42fb2b782d
commit 4b5d2a7c45
6 changed files with 952 additions and 10 deletions

220
docs/IMAGE_CLASS_USAGE.md Normal file
View File

@@ -0,0 +1,220 @@
# Image Class Usage Guide
The `Image` class provides a convenient way to load and work with images in the microscopy object detection application.
## Supported Formats
The Image class supports the following image formats:
- `.jpg`, `.jpeg` - JPEG images
- `.png` - PNG images
- `.tif`, `.tiff` - TIFF images (commonly used in microscopy)
- `.bmp` - Bitmap images
## Basic Usage
### Loading an Image
```python
from src.utils import Image, ImageLoadError
# Load an image from a file path
try:
img = Image("path/to/image.jpg")
print(f"Loaded image: {img.width}x{img.height} pixels")
except ImageLoadError as e:
print(f"Failed to load image: {e}")
```
### Accessing Image Properties
```python
img = Image("microscopy_image.tif")
# Basic properties
print(f"Width: {img.width} pixels")
print(f"Height: {img.height} pixels")
print(f"Channels: {img.channels}")
print(f"Format: {img.format}")
print(f"Shape: {img.shape}") # (height, width, channels)
# File information
print(f"File size: {img.size_mb:.2f} MB")
print(f"File size: {img.size_bytes} bytes")
# Image type checks
print(f"Is color: {img.is_color()}")
print(f"Is grayscale: {img.is_grayscale()}")
# String representation
print(img) # Shows summary of image properties
```
### Working with Image Data
```python
import numpy as np
img = Image("sample.png")
# Get image data as numpy array (OpenCV format, BGR)
bgr_data = img.data
print(f"Data shape: {bgr_data.shape}")
print(f"Data type: {bgr_data.dtype}")
# Get image as RGB (for display or processing)
rgb_data = img.get_rgb()
# Get grayscale version
gray_data = img.get_grayscale()
# Create a copy (for modifications)
img_copy = img.copy()
img_copy[0, 0] = [255, 255, 255] # Modify copy, original unchanged
# Resize image (returns new array, doesn't modify original)
resized = img.resize(640, 640)
```
### Using PIL Image
```python
img = Image("photo.jpg")
# Access as PIL Image (RGB format)
pil_img = img.pil_image
# Use PIL methods
pil_img.show() # Display image
pil_img.save("output.png") # Save with PIL
```
## Integration with YOLO
```python
from src.utils import Image
from ultralytics import YOLO
# Load model and image
model = YOLO("yolov8n.pt")
img = Image("microscopy/cell_01.tif")
# Run inference (YOLO accepts file paths or numpy arrays)
results = model(img.data)
# Or use the file path directly
results = model(str(img.path))
```
## Error Handling
```python
from src.utils import Image, ImageLoadError
def process_image(image_path):
try:
img = Image(image_path)
# Process the image...
return img
except ImageLoadError as e:
print(f"Cannot load image: {e}")
return None
```
## Advanced Usage
### Batch Processing
```python
from pathlib import Path
from src.utils import Image, ImageLoadError
def process_image_directory(directory):
"""Process all images in a directory."""
image_paths = Path(directory).glob("*.tif")
for path in image_paths:
try:
img = Image(path)
print(f"Processing {img.path.name}: {img.width}x{img.height}")
# Process the image...
except ImageLoadError as e:
print(f"Skipping {path}: {e}")
```
### Using with OpenCV Operations
```python
import cv2
from src.utils import Image
img = Image("input.jpg")
# Apply OpenCV operations on the data
blurred = cv2.GaussianBlur(img.data, (5, 5), 0)
edges = cv2.Canny(img.data, 100, 200)
# Note: These operations don't modify the original img.data
```
### Memory Efficient Processing
```python
from src.utils import Image
# The Image class loads data into memory
img = Image("large_image.tif")
print(f"Image size in memory: {img.data.nbytes / (1024**2):.2f} MB")
# When processing many images, consider loading one at a time
# and releasing memory by deleting the object
del img
```
## Best Practices
1. **Always use try-except** when loading images to handle errors gracefully
2. **Check image properties** before processing to ensure compatibility
3. **Use copy()** when you need to modify image data without affecting the original
4. **Path objects work too** - The class accepts both strings and Path objects
5. **Consider memory usage** when working with large images or batches
## Example: Complete Workflow
```python
from src.utils import Image, ImageLoadError
from src.utils.file_utils import get_image_files
def analyze_microscopy_images(directory):
"""Analyze all microscopy images in a directory."""
# Get all image files
image_files = get_image_files(directory, recursive=True)
results = []
for image_path in image_files:
try:
# Load image
img = Image(image_path)
# Analyze
result = {
'filename': img.path.name,
'width': img.width,
'height': img.height,
'channels': img.channels,
'format': img.format,
'size_mb': img.size_mb,
'is_color': img.is_color()
}
results.append(result)
print(f"✓ Analyzed {img.path.name}")
except ImageLoadError as e:
print(f"✗ Failed to load {image_path}: {e}")
return results
# Run analysis
results = analyze_microscopy_images("data/datasets/cells")
print(f"\nProcessed {len(results)} images")

151
examples/image_demo.py Normal file
View File

@@ -0,0 +1,151 @@
"""
Example script demonstrating the Image class functionality.
"""
import sys
from pathlib import Path
# Add parent directory to path for imports
sys.path.insert(0, str(Path(__file__).parent.parent))
from src.utils import Image, ImageLoadError
def demonstrate_image_loading():
"""Demonstrate basic image loading functionality."""
print("=" * 60)
print("Image Class Demonstration")
print("=" * 60)
# Example 1: Try to load an image (replace with your own path)
example_paths = [
"data/datasets/example.jpg",
"data/datasets/sample.png",
"tests/test_image.jpg",
]
loaded_img = None
for image_path in example_paths:
if Path(image_path).exists():
try:
print(f"\n1. Loading image: {image_path}")
img = Image(image_path)
loaded_img = img
print(f" ✓ Successfully loaded!")
print(f" {img}")
break
except ImageLoadError as e:
print(f" ✗ Failed: {e}")
else:
print(f"\n1. Image not found: {image_path}")
if loaded_img is None:
print("\nNo example images found. Creating a test image...")
create_test_image()
return
# Example 2: Access image properties
print(f"\n2. Image Properties:")
print(f" Width: {loaded_img.width} pixels")
print(f" Height: {loaded_img.height} pixels")
print(f" Channels: {loaded_img.channels}")
print(f" Format: {loaded_img.format.upper()}")
print(f" Shape: {loaded_img.shape}")
print(f" File size: {loaded_img.size_mb:.2f} MB")
print(f" Is color: {loaded_img.is_color()}")
print(f" Is grayscale: {loaded_img.is_grayscale()}")
# Example 3: Get different formats
print(f"\n3. Accessing Image Data:")
print(f" BGR data shape: {loaded_img.data.shape}")
print(f" RGB data shape: {loaded_img.get_rgb().shape}")
print(f" Grayscale shape: {loaded_img.get_grayscale().shape}")
print(f" PIL image mode: {loaded_img.pil_image.mode}")
# Example 4: Resizing
print(f"\n4. Resizing Image:")
resized = loaded_img.resize(320, 320)
print(f" Original size: {loaded_img.width}x{loaded_img.height}")
print(f" Resized to: {resized.shape[1]}x{resized.shape[0]}")
# Example 5: Working with copies
print(f"\n5. Creating Copies:")
copy = loaded_img.copy()
print(f" Created copy with shape: {copy.shape}")
print(f" Original data unchanged: {(loaded_img.data == copy).all()}")
print("\n" + "=" * 60)
print("Demonstration Complete!")
print("=" * 60)
def create_test_image():
"""Create a test image for demonstration purposes."""
import cv2
import numpy as np
print("\nCreating a test image...")
# Create a colorful test image
width, height = 400, 300
test_img = np.zeros((height, width, 3), dtype=np.uint8)
# Add some colors
test_img[:100, :] = [255, 0, 0] # Blue section
test_img[100:200, :] = [0, 255, 0] # Green section
test_img[200:, :] = [0, 0, 255] # Red section
# Save the test image
test_path = Path("test_demo_image.png")
cv2.imwrite(str(test_path), test_img)
print(f"Test image created: {test_path}")
# Now load and demonstrate with it
try:
img = Image(test_path)
print(f"\nLoaded test image: {img}")
print(f"Dimensions: {img.width}x{img.height}")
print(f"Channels: {img.channels}")
print(f"Format: {img.format}")
# Clean up
test_path.unlink()
print(f"\nTest image cleaned up.")
except ImageLoadError as e:
print(f"Error loading test image: {e}")
def demonstrate_error_handling():
"""Demonstrate error handling."""
print("\n" + "=" * 60)
print("Error Handling Demonstration")
print("=" * 60)
# Try to load non-existent file
print("\n1. Loading non-existent file:")
try:
img = Image("nonexistent.jpg")
except ImageLoadError as e:
print(f" ✓ Caught error: {e}")
# Try unsupported format
print("\n2. Loading unsupported format:")
try:
# Create a text file
test_file = Path("test.txt")
test_file.write_text("not an image")
img = Image(test_file)
except ImageLoadError as e:
print(f" ✓ Caught error: {e}")
test_file.unlink() # Clean up
print("\n" + "=" * 60)
if __name__ == "__main__":
print("\n")
demonstrate_image_loading()
print("\n")
demonstrate_error_handling()
print("\n")

View File

@@ -3,10 +3,27 @@ Annotation tab for the microscopy object detection application.
Future feature for manual annotation. Future feature for manual annotation.
""" """
from PySide6.QtWidgets import QWidget, QVBoxLayout, QLabel, QGroupBox from PySide6.QtWidgets import (
QWidget,
QVBoxLayout,
QHBoxLayout,
QLabel,
QGroupBox,
QPushButton,
QFileDialog,
QMessageBox,
QScrollArea,
)
from PySide6.QtGui import QPixmap, QImage
from PySide6.QtCore import Qt
from pathlib import Path
from src.database.db_manager import DatabaseManager from src.database.db_manager import DatabaseManager
from src.utils.config_manager import ConfigManager from src.utils.config_manager import ConfigManager
from src.utils.image import Image, ImageLoadError
from src.utils.logger import get_logger
logger = get_logger(__name__)
class AnnotationTab(QWidget): class AnnotationTab(QWidget):
@@ -18,6 +35,8 @@ class AnnotationTab(QWidget):
super().__init__(parent) super().__init__(parent)
self.db_manager = db_manager self.db_manager = db_manager
self.config_manager = config_manager self.config_manager = config_manager
self.current_image = None
self.current_image_path = None
self._setup_ui() self._setup_ui()
@@ -25,24 +44,165 @@ class AnnotationTab(QWidget):
"""Setup user interface.""" """Setup user interface."""
layout = QVBoxLayout() layout = QVBoxLayout()
group = QGroupBox("Annotation Tool (Future Feature)") # Image loading section
group_layout = QVBoxLayout() load_group = QGroupBox("Image Loading")
label = QLabel( load_layout = QVBoxLayout()
"Annotation functionality will be implemented in future version.\n\n"
# Load image button
button_layout = QHBoxLayout()
self.load_image_btn = QPushButton("Load Image")
self.load_image_btn.clicked.connect(self._load_image)
button_layout.addWidget(self.load_image_btn)
button_layout.addStretch()
load_layout.addLayout(button_layout)
# Image info label
self.image_info_label = QLabel("No image loaded")
load_layout.addWidget(self.image_info_label)
load_group.setLayout(load_layout)
layout.addWidget(load_group)
# Image display section
display_group = QGroupBox("Image Display")
display_layout = QVBoxLayout()
# Scroll area for image
scroll_area = QScrollArea()
scroll_area.setWidgetResizable(True)
scroll_area.setMinimumHeight(400)
self.image_label = QLabel("No image loaded")
self.image_label.setAlignment(Qt.AlignCenter)
self.image_label.setStyleSheet(
"QLabel { background-color: #2b2b2b; color: #888; }"
)
self.image_label.setScaledContents(False)
scroll_area.setWidget(self.image_label)
display_layout.addWidget(scroll_area)
display_group.setLayout(display_layout)
layout.addWidget(display_group)
# Future features info
info_group = QGroupBox("Annotation Tool (Future Feature)")
info_layout = QVBoxLayout()
info_label = QLabel(
"Full annotation functionality will be implemented in future version.\n\n"
"Planned Features:\n" "Planned Features:\n"
"- Image browser\n"
"- Drawing tools for bounding boxes\n" "- Drawing tools for bounding boxes\n"
"- Class label assignment\n" "- Class label assignment\n"
"- Export annotations to YOLO format\n" "- Export annotations to YOLO format\n"
"- Annotation verification" "- Annotation verification"
) )
group_layout.addWidget(label) info_layout.addWidget(info_label)
group.setLayout(group_layout) info_group.setLayout(info_layout)
layout.addWidget(group) layout.addWidget(info_group)
layout.addStretch()
self.setLayout(layout) self.setLayout(layout)
def _load_image(self):
"""Load and display an image file."""
# Get image repository path or use home directory
repo_path = self.config_manager.get_image_repository_path()
start_dir = repo_path if repo_path else str(Path.home())
# Open file dialog
file_path, _ = QFileDialog.getOpenFileName(
self,
"Select Image",
start_dir,
"Images (*.jpg *.jpeg *.png *.tif *.tiff *.bmp)",
)
if not file_path:
return
try:
# Load image using Image class
self.current_image = Image(file_path)
self.current_image_path = file_path
# Update info label
info_text = (
f"File: {Path(file_path).name}\n"
f"Size: {self.current_image.width}x{self.current_image.height} pixels\n"
f"Channels: {self.current_image.channels}\n"
f"Format: {self.current_image.format.upper()}\n"
f"File size: {self.current_image.size_mb:.2f} MB"
)
self.image_info_label.setText(info_text)
# Convert to QPixmap and display
self._display_image()
logger.info(f"Loaded image: {file_path}")
except ImageLoadError as e:
logger.error(f"Failed to load image: {e}")
QMessageBox.critical(
self, "Error Loading Image", f"Failed to load image:\n{str(e)}"
)
except Exception as e:
logger.error(f"Unexpected error loading image: {e}")
QMessageBox.critical(self, "Error", f"Unexpected error:\n{str(e)}")
def _display_image(self):
"""Display the current image in the image label."""
if self.current_image is None:
return
try:
# Get RGB image data
rgb_data = self.current_image.get_rgb()
# Convert numpy array to QImage
height, width, channels = rgb_data.shape
bytes_per_line = channels * width
if channels == 3:
qimage = QImage(
rgb_data.data,
width,
height,
bytes_per_line,
QImage.Format_RGB888,
)
else:
# Grayscale
qimage = QImage(
rgb_data.data,
width,
height,
bytes_per_line,
QImage.Format_Grayscale8,
)
# Convert to pixmap
pixmap = QPixmap.fromImage(qimage)
# Scale to fit display (max 800px width or height)
max_size = 800
if pixmap.width() > max_size or pixmap.height() > max_size:
pixmap = pixmap.scaled(
max_size,
max_size,
Qt.KeepAspectRatio,
Qt.SmoothTransformation,
)
# Display in label
self.image_label.setPixmap(pixmap)
self.image_label.setScaledContents(False)
except Exception as e:
logger.error(f"Error displaying image: {e}")
QMessageBox.warning(
self, "Display Error", f"Failed to display image:\n{str(e)}"
)
def refresh(self): def refresh(self):
"""Refresh the tab.""" """Refresh the tab."""
pass pass

View File

@@ -0,0 +1,7 @@
"""
Utility modules for the microscopy object detection application.
"""
from src.utils.image import Image, ImageLoadError
__all__ = ["Image", "ImageLoadError"]

259
src/utils/image.py Normal file
View File

@@ -0,0 +1,259 @@
"""
Image loading and management utilities for the microscopy object detection application.
"""
import cv2
import numpy as np
from pathlib import Path
from typing import Optional, Tuple, Union
from PIL import Image as PILImage
from src.utils.logger import get_logger
from src.utils.file_utils import validate_file_path, is_image_file
logger = get_logger(__name__)
class ImageLoadError(Exception):
"""Exception raised when an image cannot be loaded."""
pass
class Image:
"""
A class for loading and managing images from file paths.
Supports multiple image formats: .jpg, .jpeg, .png, .tif, .tiff, .bmp
Provides access to image data in multiple formats (OpenCV/numpy, PIL).
Attributes:
path: Path to the image file
data: Image data as numpy array (OpenCV format, BGR)
pil_image: Image data as PIL Image (RGB)
width: Image width in pixels
height: Image height in pixels
channels: Number of color channels
format: Image file format
size_bytes: File size in bytes
"""
SUPPORTED_EXTENSIONS = [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp"]
def __init__(self, image_path: Union[str, Path]):
"""
Initialize an Image object by loading from a file path.
Args:
image_path: Path to the image file (string or Path object)
Raises:
ImageLoadError: If the image cannot be loaded or is invalid
"""
self.path = Path(image_path)
self._data: Optional[np.ndarray] = None
self._pil_image: Optional[PILImage.Image] = None
self._width: int = 0
self._height: int = 0
self._channels: int = 0
self._format: str = ""
self._size_bytes: int = 0
# Load the image
self._load()
def _load(self) -> None:
"""
Load the image from disk.
Raises:
ImageLoadError: If the image cannot be loaded
"""
# Validate path
if not validate_file_path(str(self.path), must_exist=True):
raise ImageLoadError(f"Invalid or non-existent file path: {self.path}")
# Check file extension
if not is_image_file(str(self.path), self.SUPPORTED_EXTENSIONS):
ext = self.path.suffix.lower()
raise ImageLoadError(
f"Unsupported image format: {ext}. "
f"Supported formats: {', '.join(self.SUPPORTED_EXTENSIONS)}"
)
try:
# Load with OpenCV (returns BGR format)
self._data = cv2.imread(str(self.path), cv2.IMREAD_UNCHANGED)
if self._data is None:
raise ImageLoadError(f"Failed to load image with OpenCV: {self.path}")
# Extract metadata
self._height, self._width = self._data.shape[:2]
self._channels = self._data.shape[2] if len(self._data.shape) == 3 else 1
self._format = self.path.suffix.lower().lstrip(".")
self._size_bytes = self.path.stat().st_size
# Load PIL version for compatibility (convert BGR to RGB)
if self._channels == 3:
rgb_data = cv2.cvtColor(self._data, cv2.COLOR_BGR2RGB)
self._pil_image = PILImage.fromarray(rgb_data)
elif self._channels == 4:
rgba_data = cv2.cvtColor(self._data, cv2.COLOR_BGRA2RGBA)
self._pil_image = PILImage.fromarray(rgba_data)
else:
# Grayscale
self._pil_image = PILImage.fromarray(self._data)
logger.info(
f"Successfully loaded image: {self.path.name} "
f"({self._width}x{self._height}, {self._channels} channels, "
f"{self._format.upper()})"
)
except Exception as e:
logger.error(f"Error loading image {self.path}: {e}")
raise ImageLoadError(f"Failed to load image: {e}") from e
@property
def data(self) -> np.ndarray:
"""
Get image data as numpy array (OpenCV format, BGR or grayscale).
Returns:
Image data as numpy array
"""
if self._data is None:
raise ImageLoadError("Image data not available")
return self._data
@property
def pil_image(self) -> PILImage.Image:
"""
Get image data as PIL Image (RGB or grayscale).
Returns:
PIL Image object
"""
if self._pil_image is None:
raise ImageLoadError("PIL image not available")
return self._pil_image
@property
def width(self) -> int:
"""Get image width in pixels."""
return self._width
@property
def height(self) -> int:
"""Get image height in pixels."""
return self._height
@property
def shape(self) -> Tuple[int, int, int]:
"""
Get image shape as (height, width, channels).
Returns:
Tuple of (height, width, channels)
"""
return (self._height, self._width, self._channels)
@property
def channels(self) -> int:
"""Get number of color channels."""
return self._channels
@property
def format(self) -> str:
"""Get image file format (e.g., 'jpg', 'png')."""
return self._format
@property
def size_bytes(self) -> int:
"""Get file size in bytes."""
return self._size_bytes
@property
def size_mb(self) -> float:
"""Get file size in megabytes."""
return self._size_bytes / (1024 * 1024)
def get_rgb(self) -> np.ndarray:
"""
Get image data as RGB numpy array.
Returns:
Image data in RGB format as numpy array
"""
if self._channels == 3:
return cv2.cvtColor(self._data, cv2.COLOR_BGR2RGB)
elif self._channels == 4:
return cv2.cvtColor(self._data, cv2.COLOR_BGRA2RGBA)
else:
return self._data
def get_grayscale(self) -> np.ndarray:
"""
Get image as grayscale numpy array.
Returns:
Grayscale image as numpy array
"""
if self._channels == 1:
return self._data
else:
return cv2.cvtColor(self._data, cv2.COLOR_BGR2GRAY)
def copy(self) -> np.ndarray:
"""
Get a copy of the image data.
Returns:
Copy of image data as numpy array
"""
return self._data.copy()
def resize(self, width: int, height: int) -> np.ndarray:
"""
Resize the image to specified dimensions.
Args:
width: Target width in pixels
height: Target height in pixels
Returns:
Resized image as numpy array (does not modify original)
"""
return cv2.resize(self._data, (width, height))
def is_grayscale(self) -> bool:
"""
Check if image is grayscale.
Returns:
True if image is grayscale (1 channel)
"""
return self._channels == 1
def is_color(self) -> bool:
"""
Check if image is color.
Returns:
True if image has 3 or more channels
"""
return self._channels >= 3
def __repr__(self) -> str:
"""String representation of the Image object."""
return (
f"Image(path='{self.path.name}', "
f"shape=({self._width}x{self._height}x{self._channels}), "
f"format={self._format}, "
f"size={self.size_mb:.2f}MB)"
)
def __str__(self) -> str:
"""String representation of the Image object."""
return self.__repr__()

145
tests/test_image.py Normal file
View File

@@ -0,0 +1,145 @@
"""
Tests for the Image class.
"""
import pytest
import numpy as np
from pathlib import Path
from src.utils.image import Image, ImageLoadError
class TestImage:
"""Test cases for the Image class."""
def test_load_nonexistent_file(self):
"""Test loading a non-existent file raises ImageLoadError."""
with pytest.raises(ImageLoadError):
Image("nonexistent_file.jpg")
def test_load_unsupported_format(self, tmp_path):
"""Test loading an unsupported format raises ImageLoadError."""
# Create a dummy file with unsupported extension
test_file = tmp_path / "test.txt"
test_file.write_text("not an image")
with pytest.raises(ImageLoadError):
Image(test_file)
def test_supported_extensions(self):
"""Test that supported extensions are correctly defined."""
expected_extensions = [".jpg", ".jpeg", ".png", ".tif", ".tiff", ".bmp"]
assert Image.SUPPORTED_EXTENSIONS == expected_extensions
def test_image_properties(self, tmp_path):
"""Test image properties after loading."""
# Create a simple test image using numpy and cv2
import cv2
test_img = np.zeros((100, 200, 3), dtype=np.uint8)
test_img[:, :] = [255, 0, 0] # Blue in BGR
test_file = tmp_path / "test.jpg"
cv2.imwrite(str(test_file), test_img)
# Load the image
img = Image(test_file)
# Check properties
assert img.width == 200
assert img.height == 100
assert img.channels == 3
assert img.format == "jpg"
assert img.shape == (100, 200, 3)
assert img.size_bytes > 0
assert img.is_color()
assert not img.is_grayscale()
def test_get_rgb(self, tmp_path):
"""Test RGB conversion."""
import cv2
# Create BGR image
test_img = np.zeros((50, 50, 3), dtype=np.uint8)
test_img[:, :] = [255, 0, 0] # Blue in BGR
test_file = tmp_path / "test_rgb.png"
cv2.imwrite(str(test_file), test_img)
img = Image(test_file)
rgb_data = img.get_rgb()
# RGB should have red channel at 255
assert rgb_data[0, 0, 0] == 0 # R
assert rgb_data[0, 0, 1] == 0 # G
assert rgb_data[0, 0, 2] == 255 # B (was BGR blue)
def test_get_grayscale(self, tmp_path):
"""Test grayscale conversion."""
import cv2
test_img = np.zeros((50, 50, 3), dtype=np.uint8)
test_img[:, :] = [128, 128, 128]
test_file = tmp_path / "test_gray.png"
cv2.imwrite(str(test_file), test_img)
img = Image(test_file)
gray_data = img.get_grayscale()
assert len(gray_data.shape) == 2 # Should be 2D
assert gray_data.shape == (50, 50)
def test_copy(self, tmp_path):
"""Test copying image data."""
import cv2
test_img = np.zeros((50, 50, 3), dtype=np.uint8)
test_file = tmp_path / "test_copy.png"
cv2.imwrite(str(test_file), test_img)
img = Image(test_file)
copied = img.copy()
# Modify copy
copied[0, 0] = [255, 255, 255]
# Original should be unchanged
assert not np.array_equal(img.data[0, 0], copied[0, 0])
def test_resize(self, tmp_path):
"""Test image resizing."""
import cv2
test_img = np.zeros((100, 100, 3), dtype=np.uint8)
test_file = tmp_path / "test_resize.png"
cv2.imwrite(str(test_file), test_img)
img = Image(test_file)
resized = img.resize(50, 50)
assert resized.shape == (50, 50, 3)
# Original should be unchanged
assert img.width == 100
assert img.height == 100
def test_str_repr(self, tmp_path):
"""Test string representation."""
import cv2
test_img = np.zeros((100, 200, 3), dtype=np.uint8)
test_file = tmp_path / "test_str.jpg"
cv2.imwrite(str(test_file), test_img)
img = Image(test_file)
str_repr = str(img)
assert "test_str.jpg" in str_repr
assert "100x200x3" in str_repr
assert "jpg" in str_repr
repr_str = repr(img)
assert "Image" in repr_str
assert "test_str.jpg" in repr_str