CoolFace
Apppublic

DocForg/Document_Forgery_Detection

sourceHugging Facemitupdated 6mo agoView on Hugging Face
0likes
train_classifier_doctamper_fixed.py369 linesDownload Raw Back to scripts
1"""
2LightGBM Classifier Training - DocTamper with Tampering Labels
3FIXED VERSION with proper checkpointing and feature dimension handling
4
5Implements Algorithm Steps 7-8:
6  7. Hybrid Feature Extraction
7  8. Region-wise Forgery Classification
8
9Uses:
10- Localization: best_doctamper.pth (Steps 1-6 complete)
11- Training: DocTamper TrainingSet + tampering/DocTamperV1-TrainingSet.pk
12- Testing: DocTamper TestingSet + tampering/DocTamperV1-TestingSet.pk
13- Classes: Copy-Move (CM), Splicing (SP), Generation (GE)
14
15Features:
16- ✅ Checkpoint saving every 1000 samples
17- ✅ Resume from checkpoint if interrupted
18- ✅ Fixed feature dimension mismatch
19- ✅ Robust error handling
20
21Usage:
22    python scripts/train_classifier_doctamper_fixed.py
23"""
24
25import sys
26from pathlib import Path
27import numpy as np
28import pickle
29import lmdb
30import cv2
31import torch
32from tqdm import tqdm
33import json
34
35sys.path.insert(0, str(Path(__file__).parent.parent))
36
37from src.config import get_config
38from src.models import get_model
39from src.features import get_feature_extractor
40from src.training.classifier import get_classifier
41
42# Configuration
43MODEL_PATH = 'outputs/checkpoints/best_doctamper.pth'
44OUTPUT_DIR = 'outputs/classifier'
45MAX_SAMPLES = 999999  # Use all available samples
46
47# Label mapping (Algorithm Step 8.2) - 3 classes
48LABEL_MAP = {
49    'CM': 0,  # Copy-Move
50    'SP': 1,  # Splicing
51    'GE': 2,  # Generation (AI-generated, separate from Splicing)
52}
53
54
55def load_tampering_labels(label_file):
56    """Load forgery type labels from tampering folder"""
57    with open(label_file, 'rb') as f:
58        labels = pickle.load(f)
59    
60    print(f"Loaded {len(labels)} labels from {label_file}")
61    return labels
62
63
64def load_sample_from_lmdb(lmdb_env, index):
65    """Load image and mask from LMDB"""
66    txn = lmdb_env.begin()
67    
68    # Get image
69    img_key = f'image-{index:09d}'.encode('utf-8')
70    img_data = txn.get(img_key)
71    if not img_data:
72        return None, None
73    
74    # Get mask (DocTamper uses 'label-' not 'mask-')
75    mask_key = f'label-{index:09d}'.encode('utf-8')
76    mask_data = txn.get(mask_key)
77    if not mask_data:
78        return None, None
79    
80    # Decode
81    img_array = np.frombuffer(img_data, dtype=np.uint8)
82    image = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
83    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
84    
85    mask_array = np.frombuffer(mask_data, dtype=np.uint8)
86    mask = cv2.imdecode(mask_array, cv2.IMREAD_GRAYSCALE)
87    
88    return image, mask
89
90
91def extract_features(config, model, lmdb_path, tampering_labels, 
92                    max_samples, device, split_name):
93    """
94    Extract hybrid features with checkpointing and resume capability
95    """
96    
97    print(f"\n{'='*60}")
98    print(f"Extracting features from {split_name}")
99    print(f"{'='*60}")
100    
101    # Setup checkpoint directory
102    checkpoint_dir = Path(OUTPUT_DIR)
103    checkpoint_dir.mkdir(parents=True, exist_ok=True)
104    
105    # Check for existing checkpoint to resume
106    checkpoints = list(checkpoint_dir.glob(f'checkpoint_{split_name}_*.npz'))
107    if checkpoints:
108        latest_checkpoint = max(checkpoints, key=lambda p: int(p.stem.split('_')[-1]))
109        print(f"✓ Found checkpoint: {latest_checkpoint.name}")
110        
111        data = np.load(latest_checkpoint, allow_pickle=True)
112        all_features = data['features'].tolist()
113        all_labels = data['labels'].tolist()
114        expected_dim = int(data['feature_dim'])
115        start_idx = len(all_features)
116        
117        print(f"✓ Resuming from sample {start_idx}, feature_dim={expected_dim}")
118    else:
119        all_features = []
120        all_labels = []
121        expected_dim = None
122        start_idx = 0
123    
124    # Open LMDB
125    env = lmdb.open(lmdb_path, readonly=True, lock=False)
126    
127    # Initialize feature extractor
128    feature_extractor = get_feature_extractor(config, is_text_document=True)
129    
130    # Process samples
131    num_processed = start_idx
132    dim_mismatch_count = 0
133    
134    for i in tqdm(range(start_idx, min(len(tampering_labels), max_samples)), 
135                  desc=f"Processing {split_name}", initial=start_idx, 
136                  total=min(len(tampering_labels), max_samples)):
137        try:
138            # Skip if no label
139            if i not in tampering_labels:
140                continue
141            
142            # Get forgery type label
143            forgery_type = tampering_labels[i]
144            if forgery_type not in LABEL_MAP:
145                continue
146            
147            label = LABEL_MAP[forgery_type]
148            
149            # Load image and mask
150            image, mask = load_sample_from_lmdb(env, i)
151            if image is None or mask is None:
152                continue
153            
154            # Skip if no forgery
155            if mask.max() == 0:
156                continue
157            
158            # Prepare for model
159            image_tensor = torch.from_numpy(image).permute(2, 0, 1).float() / 255.0
160            image_tensor = image_tensor.unsqueeze(0).to(device)
161            
162            # Get deep features from localization model
163            with torch.no_grad():
164                logits, decoder_features = model(image_tensor)
165            
166            # Use ground truth mask for feature extraction
167            mask_binary = (mask > 127).astype(np.uint8)
168            
169            # Extract hybrid features
170            features = feature_extractor.extract(
171                image / 255.0,
172                mask_binary,
173                [f.cpu() for f in decoder_features]
174            )
175            
176            # Set expected dimension from first valid sample
177            if expected_dim is None:
178                expected_dim = len(features)
179                print(f"\n✓ Feature dimension set to: {expected_dim}")
180            
181            # Ensure consistent feature dimension
182            if len(features) != expected_dim:
183                if len(features) < expected_dim:
184                    features = np.pad(features, (0, expected_dim - len(features)), mode='constant')
185                else:
186                    features = features[:expected_dim]
187                dim_mismatch_count += 1
188            
189            all_features.append(features)
190            all_labels.append(label)
191            num_processed += 1
192            
193            # Save checkpoint every 10,000 samples (only 12 checkpoints total)
194            if num_processed % 10000 == 0:
195                checkpoint_path = checkpoint_dir / f'checkpoint_{split_name}_{num_processed}.npz'
196                features_array = np.array(all_features, dtype=np.float32)
197                labels_array = np.array(all_labels, dtype=np.int32)
198                
199                np.savez_compressed(checkpoint_path,
200                                   features=features_array,
201                                   labels=labels_array,
202                                   feature_dim=expected_dim)
203                print(f"\n✓ Checkpoint: {num_processed} samples (dim={expected_dim}, mismatches={dim_mismatch_count})")
204                
205                # Delete old checkpoints to save space (keep only last 2)
206                old_checkpoints = sorted(checkpoint_dir.glob(f'checkpoint_{split_name}_*.npz'))
207                if len(old_checkpoints) > 2:
208                    for old_cp in old_checkpoints[:-2]:
209                        old_cp.unlink()
210                        print(f"  Cleaned up: {old_cp.name}")
211        
212        except Exception as e:
213            print(f"\n⚠ Error at sample {i}: {str(e)[:80]}")
214            continue
215    
216    env.close()
217    
218    print(f"\n✓ Extracted {num_processed} samples")
219    if dim_mismatch_count > 0:
220        print(f"⚠ Fixed {dim_mismatch_count} dimension mismatches")
221    
222    # Save final features
223    final_path = checkpoint_dir / f'features_{split_name}_final.npz'
224    if len(all_features) > 0:
225        features_array = np.array(all_features, dtype=np.float32)
226        labels_array = np.array(all_labels, dtype=np.int32)
227        
228        np.savez_compressed(final_path,
229                           features=features_array,
230                           labels=labels_array,
231                           feature_dim=expected_dim)
232        print(f"✓ Final features saved: {final_path}")
233        print(f"  Shape: features={features_array.shape}, labels={labels_array.shape}")
234        
235        return features_array, labels_array
236    
237    return None, None
238
239
240def main():
241    config = get_config('config.yaml')
242    device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
243    
244    print("\n" + "="*60)
245    print("LightGBM Classifier Training - DocTamper (FIXED)")
246    print("Implements Algorithm Steps 7-8")
247    print("="*60)
248    print(f"Model: {MODEL_PATH}")
249    print(f"Device: {device}")
250    print(f"Max samples: {MAX_SAMPLES}")
251    print("="*60)
252    print("\nForgery Type Classes (Step 8.2):")
253    print("  0: Copy-Move (CM)")
254    print("  1: Splicing (SP)")
255    print("  2: Generation (GE)")
256    print("="*60)
257    
258    # Load localization model
259    print("\nLoading localization model...")
260    model = get_model(config).to(device)
261    checkpoint = torch.load(MODEL_PATH, map_location=device)
262    model.load_state_dict(checkpoint['model_state_dict'])
263    model.eval()
264    print(f"✓ Model loaded (Val Dice: {checkpoint.get('best_metric', 0):.4f})")
265    
266    # Load tampering labels
267    train_labels = load_tampering_labels(
268        'datasets/DocTamper/tampering/DocTamperV1-TrainingSet.pk'
269    )
270    test_labels = load_tampering_labels(
271        'datasets/DocTamper/tampering/DocTamperV1-TestingSet.pk'
272    )
273    
274    # Extract features from TrainingSet
275    X_train, y_train = extract_features(
276        config, model,
277        'datasets/DocTamper/DocTamperV1-TrainingSet',
278        train_labels,
279        MAX_SAMPLES,
280        device,
281        'TrainingSet'
282    )
283    
284    # Extract features from TestingSet
285    X_test, y_test = extract_features(
286        config, model,
287        'datasets/DocTamper/DocTamperV1-TestingSet',
288        test_labels,
289        MAX_SAMPLES // 4,
290        device,
291        'TestingSet'
292    )
293    
294    if X_train is None or X_test is None:
295        print("\n❌ No features extracted!")
296        return
297    
298    # Summary
299    print("\n" + "="*60)
300    print("Dataset Summary")
301    print("="*60)
302    print(f"Training samples: {len(X_train):,}")
303    print(f"Testing samples: {len(X_test):,}")
304    print(f"Feature dimension: {X_train.shape[1]}")
305    
306    print(f"\nTraining class distribution:")
307    train_counts = np.bincount(y_train)
308    class_names = ['Copy-Move', 'Splicing', 'Generation']
309    for i, count in enumerate(train_counts):
310        if i < len(class_names):
311            print(f"  {class_names[i]}: {count:,} ({count/len(y_train)*100:.1f}%)")
312    
313    print(f"\nTesting class distribution:")
314    test_counts = np.bincount(y_test)
315    for i, count in enumerate(test_counts):
316        if i < len(class_names):
317            print(f"  {class_names[i]}: {count:,} ({count/len(y_test)*100:.1f}%)")
318    
319    # Train classifier
320    print("\n" + "="*60)
321    print("Training LightGBM Classifier (Step 8.1)")
322    print("="*60)
323    
324    output_dir = Path(OUTPUT_DIR)
325    output_dir.mkdir(parents=True, exist_ok=True)
326    
327    classifier = get_classifier(config)
328    feature_names = get_feature_extractor(config, is_text_document=True).get_feature_names()
329    
330    # Combine train and test for sklearn train_test_split
331    X_combined = np.vstack([X_train, X_test])
332    y_combined = np.concatenate([y_train, y_test])
333    
334    metrics = classifier.train(X_combined, y_combined, feature_names=feature_names)
335    
336    # Save results
337    classifier.save(str(output_dir))
338    print(f"\n✓ Classifier saved to: {output_dir}")
339    
340    # Save metrics
341    metrics_path = output_dir / 'training_metrics.json'
342    with open(metrics_path, 'w') as f:
343        json.dump(metrics, f, indent=2)
344    
345    # Save class mapping
346    class_mapping = {
347        0: 'Copy-Move',
348        1: 'Splicing',
349        2: 'Generation'
350    }
351    mapping_path = output_dir / 'class_mapping.json'
352    with open(mapping_path, 'w') as f:
353        json.dump(class_mapping, f, indent=2)
354    
355    print("\n" + "="*60)
356    print("✅ Classifier Training Complete!")
357    print("Algorithm Steps 7-8: DONE")
358    print("="*60)
359    print(f"\nResults:")
360    print(f"  Test Accuracy: {metrics.get('test_accuracy', 'N/A')}")
361    print(f"  Test F1 Score: {metrics.get('test_f1', 'N/A')}")
362    print(f"\nOutput: {output_dir}")
363    print("\nNext: Implement Steps 9-11 in inference pipeline")
364    print("="*60 + "\n")
365
366
367if __name__ == '__main__':
368    main()
369