File size: 6,830 Bytes
22997db
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
import logging
import shutil
from pathlib import Path
from typing import Optional
import cv2
import numpy as np
from tqdm import tqdm

class AZProcessor:
    def __init__(self):
        self.logger = logging.getLogger(__name__)
        self.project_root = Path(__file__).parent.parent.parent.parent
        self.source_path = self.project_root / "data" / "raw" / "az_handwritten"
        self.temp_path = self.project_root / "data" / "temp" / "AZ"

    def _normalize_documentation(self) -> None:
        """Normalize documentation to simplified format (filename label)."""
        doc_path = self.temp_path / "documentation.txt"
        normalized_path = self.temp_path / "normalized_documentation.txt"
        
        with open(doc_path, 'r') as f, open(normalized_path, 'w') as out:
            for line in f:
                if line.startswith('#') or not line.strip():
                    continue
                    
                parts = line.strip().split()
                if len(parts) >= 2:
                    filename = parts[0]        # First column (filename)
                    label = parts[-1].upper()          # Last column (label)
                    out.write(f"{filename} {label}\n")

        normalized_path.replace(doc_path)
        self.logger.info("Documentation normalized to simplified format")
        
    def process(self) -> bool:
        try:
            self.logger.info("Processing A-Z Handwritten dataset...")
            self.temp_path.mkdir(parents=True, exist_ok=True)
            
            if not self.source_path.exists():
                self.logger.error(f"Source dataset not found at {self.source_path}")
                return False
                
            self._copy_dataset()
            self._normalize_documentation()
            
            image_files = list((self.temp_path / "images").glob("*.png"))
            total_files = len(image_files)
            
            for idx, img_path in enumerate(tqdm(image_files, desc="Processing A-Z")):
                try:
                    image = cv2.imread(str(img_path), cv2.IMREAD_GRAYSCALE)
                    if image is None:
                        self.logger.warning(f"Failed to read image: {img_path}")
                        continue
                        
                    processed = self._process_single_image(image)
                    cv2.imwrite(str(img_path), processed)
                    
                    progress = (idx + 1) / total_files * 100
                    self.logger.debug(f"Processed {img_path.name} ({progress:.2f}%)")
                    
                except Exception as e:
                    self.logger.error(f"Error processing {img_path}: {str(e)}")
                    continue
                    
            self.logger.info("A-Z processing completed successfully")
            return True
            
        except Exception as e:
            self.logger.error(f"Error in A-Z processing: {str(e)}")
            return False

    def _process_single_image(self, image: np.ndarray) -> np.ndarray:
        """Process single A-Z image through enhancement pipeline."""
        image = self._enhance_contrast(image)
        image = self._reduce_noise(image)
        image = self._center_character(image)
        image = self._normalize_stroke(image)
        return image

    def _enhance_contrast(self, image: np.ndarray) -> np.ndarray:
        """Enhance contrast using CLAHE."""
        clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8,8))
        return clahe.apply(image)

    def _reduce_noise(self, image: np.ndarray) -> np.ndarray:
        """Remove noise preserving edges."""
        denoised = cv2.bilateralFilter(image, d=5, sigmaColor=75, sigmaSpace=75)
        _, binary = cv2.threshold(denoised, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
        return binary

    def _center_character(self, image: np.ndarray) -> np.ndarray:
        """Center character using moments."""
        contours, _ = cv2.findContours(image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        if not contours:
            return image
            
        largest_contour = max(contours, key=cv2.contourArea)
        M = cv2.moments(largest_contour)
        
        if M["m00"] == 0:
            return image
            
        cx = int(M["m10"] / M["m00"])
        cy = int(M["m01"] / M["m00"])
        
        rows, cols = image.shape
        shift_x = (cols // 2) - cx
        shift_y = (rows // 2) - cy
        
        M = np.float32([[1, 0, shift_x], [0, 1, shift_y]])
        return cv2.warpAffine(image, M, (cols, rows))

    def _normalize_stroke(self, image: np.ndarray) -> np.ndarray:
        """Normalize stroke width."""
        kernel_size = max(2, min(image.shape) // 30)
        kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size))
        return cv2.morphologyEx(image, cv2.MORPH_CLOSE, kernel)

    def _copy_dataset(self) -> None:
        """Copy dataset to temp directory."""
        if self.source_path.exists():
            shutil.copytree(self.source_path, self.temp_path, dirs_exist_ok=True)
            self.logger.info(f"Copied dataset to {self.temp_path}")
        else:
            raise FileNotFoundError(f"Source dataset not found at {self.source_path}")

def test_processor():
    """Test AZ processor functionality."""
    import tempfile
    
    test_dir = Path(tempfile.mkdtemp())
    
    try:
        img_dir = test_dir / "data" / "raw" / "az_handwritten" / "images"
        img_dir.mkdir(parents=True)
        
        # Create test image
        test_image = np.zeros((100, 100), dtype=np.uint8)
        cv2.putText(test_image, "A", (25, 75), cv2.FONT_HERSHEY_SIMPLEX, 2, 255, 2)
        
        # Add noise
        noise = np.random.normal(0, 25, test_image.shape).astype(np.uint8)
        noisy_image = cv2.add(test_image, noise)
        
        cv2.imwrite(str(img_dir / "test.png"), noisy_image)
        
        processor = AZProcessor()
        processor.project_root = test_dir
        processor.source_path = test_dir / "data" / "raw" / "az_handwritten"
        processor.temp_path = test_dir / "data" / "temp" / "AZ"
        
        assert processor.process() == True
        
        processed_path = processor.temp_path / "images" / "test.png"
        assert processed_path.exists()
        
        processed_img = cv2.imread(str(processed_path), cv2.IMREAD_GRAYSCALE)
        assert processed_img is not None
        assert np.mean(processed_img) > 0
        assert len(np.unique(processed_img)) > 1
        
        print("All tests passed!")
        
    finally:
        shutil.rmtree(test_dir)

if __name__ == "__main__":
    logging.basicConfig(
        level=logging.INFO,
        format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    test_processor()