|
|
""" |
|
|
Validation module - Handles all edge cases and input validation |
|
|
""" |
|
|
|
|
|
import os |
|
|
from PIL import Image |
|
|
from .config import MAX_FILE_SIZE_MB, MIN_IMAGE_SIZE_PX, VALID_EXTENSIONS |
|
|
|
|
|
class FishImageValidator: |
|
|
"""Comprehensive image validation with edge case handling""" |
|
|
|
|
|
def __init__(self, max_size_mb=MAX_FILE_SIZE_MB, |
|
|
min_size_px=MIN_IMAGE_SIZE_PX, |
|
|
valid_extensions=VALID_EXTENSIONS): |
|
|
self.max_size_mb = max_size_mb |
|
|
self.min_size_px = min_size_px |
|
|
self.valid_extensions = valid_extensions |
|
|
|
|
|
def validate_file(self, file_path): |
|
|
""" |
|
|
Validate file exists, type, size, and image integrity |
|
|
|
|
|
Returns: |
|
|
tuple: (is_valid: bool, message: str, image: PIL.Image or None) |
|
|
""" |
|
|
|
|
|
if not os.path.exists(file_path): |
|
|
return False, "β File not found", None |
|
|
|
|
|
|
|
|
if not any(file_path.lower().endswith(ext.lower()) for ext in self.valid_extensions): |
|
|
return False, f"β Invalid file type. Accepted: {', '.join(self.valid_extensions)}", None |
|
|
|
|
|
|
|
|
try: |
|
|
file_size_mb = os.path.getsize(file_path) / (1024 * 1024) |
|
|
if file_size_mb > self.max_size_mb: |
|
|
return False, f"β File too large ({file_size_mb:.1f}MB). Max: {self.max_size_mb}MB", None |
|
|
except Exception as e: |
|
|
return False, f"β Cannot read file: {e}", None |
|
|
|
|
|
|
|
|
try: |
|
|
img = Image.open(file_path) |
|
|
img.verify() |
|
|
img = Image.open(file_path) |
|
|
img = img.convert('RGB') |
|
|
|
|
|
|
|
|
if img.width < self.min_size_px or img.height < self.min_size_px: |
|
|
return False, f"β Image too small ({img.width}x{img.height}px). Min: {self.min_size_px}x{self.min_size_px}px", None |
|
|
|
|
|
return True, "β
File validation passed", img |
|
|
|
|
|
except Exception as e: |
|
|
return False, f"β Invalid or corrupted image: {str(e)}", None |
|
|
|
|
|
def validate_with_gemini(self, image, gemini_model): |
|
|
""" |
|
|
AI-based validation with Gemini Vision |
|
|
Handles edge cases: multiple fish, dead fish, toys, drawings, partial fish |
|
|
Accepts dataset images with transparent or solid backgrounds |
|
|
|
|
|
Returns: |
|
|
tuple: (is_valid: bool, message: str) |
|
|
""" |
|
|
if gemini_model is None: |
|
|
return True, "β οΈ Gemini validation disabled" |
|
|
|
|
|
try: |
|
|
prompt = """Analyze this image for fish disease diagnosis. |
|
|
|
|
|
Answer these questions: |
|
|
|
|
|
1. Is there a FISH visible in this image? (Can be a real photo, medical/diagnostic image, or isolated fish specimen on any background including transparent/solid backgrounds) |
|
|
2. How many fish are in the image? |
|
|
3. Is the fish body clearly visible (not just head or tail)? |
|
|
4. Can you see enough fish detail for disease assessment? |
|
|
|
|
|
Respond in this EXACT format: |
|
|
CONTAINS_FISH: YES or NO |
|
|
FISH_COUNT: [number] |
|
|
BODY_VISIBLE: YES or NO |
|
|
SUFFICIENT_DETAIL: YES or NO |
|
|
REASON: [brief explanation if any answer is NO] |
|
|
|
|
|
IMPORTANT NOTES: |
|
|
- Isolated fish on transparent, white, or solid backgrounds ARE ACCEPTABLE (common in medical datasets) |
|
|
- Focus on whether the FISH ITSELF is clear and detailed, not the background |
|
|
- Reject only if it's clearly NOT a fish (toy, cartoon, drawing of non-fish subject)""" |
|
|
|
|
|
response = gemini_model.generate_content([prompt, image]) |
|
|
answer = response.text.strip().upper() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if "CONTAINS_FISH: NO" in answer: |
|
|
reason = self._extract_reason(answer) |
|
|
return False, f"β No fish detected. {reason}" |
|
|
|
|
|
|
|
|
if "FISH_COUNT: 0" in answer or "FISH_COUNT: NONE" in answer: |
|
|
return False, "β No fish found in image" |
|
|
|
|
|
|
|
|
for i in range(2, 20): |
|
|
if f"FISH_COUNT: {i}" in answer: |
|
|
return False, "β Multiple fish detected. Upload single fish only" |
|
|
|
|
|
|
|
|
if "BODY_VISIBLE: NO" in answer: |
|
|
return False, "β Fish body not clearly visible" |
|
|
|
|
|
|
|
|
if "SUFFICIENT_DETAIL: NO" in answer: |
|
|
reason = self._extract_reason(answer) |
|
|
return False, f"β Insufficient detail for diagnosis. {reason}" |
|
|
|
|
|
return True, "β
Valid fish image detected" |
|
|
|
|
|
except Exception as e: |
|
|
|
|
|
print(f"β οΈ Gemini validation error: {e}") |
|
|
return True, "β οΈ AI validation skipped (error occurred)" |
|
|
|
|
|
@staticmethod |
|
|
def _extract_reason(response_text): |
|
|
"""Extract reason from Gemini response""" |
|
|
if "REASON:" in response_text: |
|
|
reason = response_text.split("REASON:")[-1].strip() |
|
|
return reason[:150] |
|
|
return "See validation details" |
|
|
|