Kalhar.Pandya commited on
Commit
2ae889c
·
0 Parent(s):

Initial commit

Browse files
Files changed (5) hide show
  1. .gitattributes +1 -0
  2. .gradio/certificate.pem +31 -0
  3. app.py +156 -0
  4. feature_extractor.py +201 -0
  5. svm_model_color.pkl +3 -0
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.pkl filter=lfs diff=lfs merge=lfs -text
.gradio/certificate.pem ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -----BEGIN CERTIFICATE-----
2
+ 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
31
+ -----END CERTIFICATE-----
app.py ADDED
@@ -0,0 +1,156 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ import pickle
5
+ import gradio as gr
6
+
7
+ # Import the feature extraction function from feature_extractor.py
8
+ from feature_extractor import extract_features_from_image
9
+
10
+ # Global variables for the classifier, class names, and training log
11
+ classifier = None
12
+ class_names = []
13
+ training_log = ""
14
+
15
+ # ---------------------------------------------------------------------
16
+ # Model Loading
17
+ # ---------------------------------------------------------------------
18
+ def load_model(model_filename):
19
+ global classifier, class_names, training_log
20
+ if os.path.exists(model_filename):
21
+ print("Found existing SVM model. Loading...")
22
+ with open(model_filename, "rb") as f:
23
+ model_data = pickle.load(f)
24
+ classifier = model_data['classifier']
25
+ class_names = model_data['class_names']
26
+ training_log += "Loaded model from disk.\n"
27
+ print("Loaded SVM model from disk.")
28
+ else:
29
+ print(f"Model file {model_filename} not found. Please train the model first.")
30
+
31
+ def classify_new_image(input_image_path):
32
+ """
33
+ Expects input_image_path as a file path. Loads the image,
34
+ processes it, and returns the final prediction and probabilities.
35
+ """
36
+ global classifier, training_log, class_names
37
+ progress_log = training_log + "\nStarting classification...\n"
38
+
39
+ # Load image using OpenCV from file path
40
+ image = cv2.imread(input_image_path)
41
+ if image is None:
42
+ raise ValueError("Error: Could not load image from the provided file path.")
43
+
44
+ # Resize the image to a fixed width (1000 px) while maintaining aspect ratio
45
+ fixed_width = 1000
46
+ height, width = image.shape[:2]
47
+ aspect_ratio = height / width
48
+ new_height = int(fixed_width * aspect_ratio)
49
+ resized_image = cv2.resize(image, (fixed_width, new_height))
50
+ progress_log += "Resized image to fixed width of 1000 pixels.\n"
51
+
52
+ # The image from cv2.imread is already in BGR format.
53
+ image_bgr = resized_image
54
+ progress_log += "Image loaded in BGR format.\n"
55
+
56
+ # Preprocessing – Convert to grayscale, apply Gaussian blur, and compute edges
57
+ gray = cv2.cvtColor(image_bgr, cv2.COLOR_BGR2GRAY)
58
+ blurred = cv2.GaussianBlur(gray, (9, 9), 0)
59
+ edges = cv2.Canny(blurred, threshold1=0, threshold2=100)
60
+ progress_log += "Computed edges using Canny edge detection.\n"
61
+
62
+ # Patch extraction parameters
63
+ patch_size = (100, 100)
64
+ edge_density_thresh_low = 0.0
65
+ edge_density_thresh_high = 0.5
66
+ patch_w, patch_h = patch_size
67
+ img_h, img_w = gray.shape
68
+
69
+ valid_patch_count = 0
70
+ summed_probabilities = None
71
+
72
+ # Loop over non-overlapping patches
73
+ for y in range(0, img_h - patch_h + 1, patch_h):
74
+ for x in range(0, img_w - patch_w + 1, patch_w):
75
+ patch_edges = edges[y:y+patch_h, x:x+patch_w]
76
+ patch = resized_image[y:y+patch_h, x:x+patch_w]
77
+ num_edge_pixels = np.sum(patch_edges > 0)
78
+ total_pixels = patch_w * patch_h
79
+ density = num_edge_pixels / total_pixels
80
+ progress_log += f"Patch at ({x}, {y}) - edge density: {density:.3f}\n"
81
+
82
+ if edge_density_thresh_low < density < edge_density_thresh_high:
83
+ valid_patch_count += 1
84
+ features = extract_features_from_image(patch)
85
+ feature_vector = features['combined_features'].reshape(1, -1)
86
+ patch_probabilities = classifier.predict_proba(feature_vector)[0]
87
+ progress_log += f"Patch at ({x}, {y}) predicted probabilities: {patch_probabilities}\n"
88
+
89
+ if summed_probabilities is None:
90
+ summed_probabilities = patch_probabilities
91
+ else:
92
+ summed_probabilities += patch_probabilities
93
+
94
+ # Fallback: if no valid patches are found, classify the whole image.
95
+ if valid_patch_count == 0:
96
+ progress_log += "No valid patches found. Falling back to whole image classification.\n"
97
+ features = extract_features_from_image(image_bgr)
98
+ feature_vector = features['combined_features'].reshape(1, -1)
99
+ summed_probabilities = classifier.predict_proba(feature_vector)[0]
100
+ valid_patch_count = 1
101
+
102
+ # Average the probabilities from all valid patches
103
+ averaged_probabilities = summed_probabilities / valid_patch_count
104
+
105
+ # Normalize the averaged probabilities so they sum to 1
106
+ normalized_probabilities = averaged_probabilities / np.sum(averaged_probabilities)
107
+
108
+ final_prediction_index = np.argmax(normalized_probabilities)
109
+ final_prediction = class_names[final_prediction_index]
110
+ prob_dict = {cls: float(normalized_probabilities[i]) for i, cls in enumerate(class_names)}
111
+ progress_log += "Classification completed.\n"
112
+
113
+ print(progress_log)
114
+ print(prob_dict)
115
+ return final_prediction, prob_dict
116
+
117
+
118
+ # Gradio Interface Setup using file paths
119
+ if __name__ == "__main__":
120
+ model_filename = "svm_model_color.pkl"
121
+ load_model(model_filename)
122
+
123
+ iface = gr.Interface(
124
+ fn=classify_new_image,
125
+ inputs=gr.Image(type="filepath"),
126
+ outputs=[
127
+ gr.Label(label="Predicted Class"),
128
+ gr.Label(label="Probabilities")
129
+ ],
130
+ title="Stone, Wood, Brick Classifier",
131
+ description=("Upload an image of stone, wood, or brick to classify it.\n\n"
132
+ "The image is processed by subdividing it into patches and aggregating the predictions. "
133
+ "Progress logs are printed to the terminal.")
134
+ )
135
+ iface.launch(share=True)
136
+
137
+ # ---------------------------------------------------------------------
138
+ # Gradio Interface Setup
139
+ # ---------------------------------------------------------------------
140
+ if __name__ == "__main__":
141
+ model_filename = "svm_model2.pkl"
142
+ load_model(model_filename)
143
+
144
+ iface = gr.Interface(
145
+ fn=classify_new_image,
146
+ inputs=gr.Image(type="filepath"),
147
+ outputs=[
148
+ gr.Label(label="Predicted Class"),
149
+ gr.Label(label="Probabilities")
150
+ ],
151
+ title="Stone, Wood, Brick Classifier",
152
+ description=("Upload an image of stone, wood, or brick to classify it.\n\n"
153
+ "The image is processed by subdividing it into patches and aggregating the predictions. "
154
+ "Progress logs are printed to the terminal.")
155
+ )
156
+ iface.launch()
feature_extractor.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import cv2
3
+ import numpy as np
4
+ from skimage.feature import local_binary_pattern, graycomatrix, graycoprops
5
+
6
+ # ---------------------------------------------------------------------
7
+ # 1. Color Features
8
+ # ---------------------------------------------------------------------
9
+ def get_average_color(image):
10
+ """
11
+ Compute the average color in BGR format (3 values).
12
+ """
13
+ return np.mean(image, axis=(0, 1)) # shape: (3,)
14
+
15
+ def get_small_color_hist(image, h_bins=8, s_bins=2, v_bins=2):
16
+ """
17
+ Compute a *reduced* color histogram in HSV space:
18
+ - h_bins: number of bins for Hue
19
+ - s_bins: number of bins for Saturation
20
+ - v_bins: number of bins for Value
21
+
22
+ Total bins = h_bins * s_bins * v_bins.
23
+ """
24
+ hsv = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)
25
+ hist = cv2.calcHist(
26
+ [hsv],
27
+ [0, 1, 2],
28
+ None,
29
+ [h_bins, s_bins, v_bins],
30
+ [0, 180, 0, 256, 0, 256]
31
+ )
32
+ cv2.normalize(hist, hist)
33
+ return hist.flatten() # shape: (h_bins*s_bins*v_bins,)
34
+
35
+ # ---------------------------------------------------------------------
36
+ # 2. LBP (Local Binary Patterns)
37
+ # ---------------------------------------------------------------------
38
+ def get_lbp_histogram(image, num_points, radius):
39
+ # Ensure the image is grayscale: only convert if it has more than one channel.
40
+ if len(image.shape) > 2 and image.shape[2] != 1:
41
+ gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
42
+ else:
43
+ gray = image
44
+
45
+ # Compute the LBP representation of the image.
46
+ # (Assuming you're using skimage's local_binary_pattern)
47
+ from skimage.feature import local_binary_pattern
48
+ lbp = local_binary_pattern(gray, num_points, radius, method="uniform")
49
+
50
+ # Build the histogram of the LBP.
51
+ n_bins = int(num_points + 2)
52
+ hist, _ = np.histogram(lbp.ravel(), bins=n_bins, range=(0, n_bins))
53
+
54
+ # Normalize the histogram.
55
+ hist = hist.astype("float")
56
+ hist /= (hist.sum() + 1e-6)
57
+
58
+ return hist
59
+
60
+ # ---------------------------------------------------------------------
61
+ # 3. GLCM (Gray Level Co-occurrence Matrix)
62
+ # ---------------------------------------------------------------------
63
+ def get_glcm_features(image,
64
+ distance=1,
65
+ angles=[0],
66
+ properties=('contrast', 'homogeneity', 'energy', 'correlation')):
67
+ """
68
+ Compute a small set of GLCM features:
69
+ - distance=1, angles=[0] (or [0, np.pi/2] if you want more orientations)
70
+ - properties = a reduced subset for simpler texture capture
71
+
72
+ Returns a flattened array of property values across all angles.
73
+ """
74
+ glcm = graycomatrix(
75
+ image,
76
+ distances=[distance],
77
+ angles=angles,
78
+ levels=256,
79
+ symmetric=True,
80
+ normed=True
81
+ )
82
+ feats = []
83
+ for prop in properties:
84
+ vals = graycoprops(glcm, prop)
85
+ feats.append(vals.ravel()) # Flatten N-dim array
86
+ glcm_features = np.concatenate(feats)
87
+ return glcm_features # shape: (len(properties)*len(angles),)
88
+
89
+ # ---------------------------------------------------------------------
90
+ # 4. Combined Feature Extraction
91
+ # ---------------------------------------------------------------------
92
+ def extract_features_from_image(image):
93
+ """
94
+ Returns a DICTIONARY of feature sets:
95
+ - 'average_color': 3 values (B, G, R) from the original image.
96
+ - 'lbp_hist': Histogram from Local Binary Patterns (texture).
97
+ - 'glcm_features': GLCM features (contrast, homogeneity, energy, correlation).
98
+ - 'edge_density': Scalar representing the fraction of edge pixels.
99
+ - 'edge_orient_hist': Normalized histogram (8 bins) of edge orientations.
100
+ - 'combined_features': Concatenation of all the above features.
101
+
102
+ This function supports both color and grayscale images.
103
+ """
104
+ import cv2
105
+ import numpy as np
106
+
107
+ # --- (A) Color Features ---
108
+ # Ensure a 3-channel image for color feature extraction.
109
+ if len(image.shape) == 2 or image.shape[2] == 1:
110
+ image_color = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
111
+ else:
112
+ image_color = image
113
+ avg_color = get_average_color(image_color) # Expected shape: (3,)
114
+
115
+ # --- (B) Texture Features: LBP and GLCM ---
116
+ # Use grayscale image for texture features.
117
+ if len(image.shape) == 2:
118
+ gray_image = image
119
+ else:
120
+ gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
121
+
122
+ lbp_hist = get_lbp_histogram(gray_image, num_points=8, radius=1) # e.g., shape = (10,)
123
+ glcm_feats = get_glcm_features(
124
+ gray_image,
125
+ distance=1,
126
+ angles=[0], # Single orientation
127
+ properties=('contrast', 'homogeneity', 'energy', 'correlation')
128
+ ) # e.g., shape = (4,)
129
+
130
+ # --- (C) Edge-Related Features ---
131
+ # Preprocessing: Blur to reduce noise before edge detection.
132
+ blurred = cv2.GaussianBlur(gray_image, (9, 9), 0)
133
+
134
+ # Compute Canny edges using fixed thresholds (these might be adapted based on context).
135
+ threshold1, threshold2 = 0, 100
136
+ edges = cv2.Canny(blurred, threshold1=threshold1, threshold2=threshold2)
137
+
138
+ # Edge Density: Ratio of edge pixels to total pixels.
139
+ edge_density = np.sum(edges > 0) / float(edges.size)
140
+
141
+ # Compute gradient orientations using Sobel operators.
142
+ grad_x = cv2.Sobel(blurred, cv2.CV_64F, 1, 0, ksize=3)
143
+ grad_y = cv2.Sobel(blurred, cv2.CV_64F, 0, 1, ksize=3)
144
+ magnitude, angle = cv2.cartToPolar(grad_x, grad_y, angleInDegrees=True)
145
+
146
+ # Use only edge pixels for orientation histogram.
147
+ angles = angle[edges > 0]
148
+ hist_bins = 8
149
+ if angles.size > 0:
150
+ edge_orient_hist, _ = np.histogram(angles, bins=hist_bins, range=(0, 360))
151
+ edge_orient_hist = edge_orient_hist.astype("float")
152
+ edge_orient_hist /= (edge_orient_hist.sum() + 1e-6) # Normalize histogram.
153
+ else:
154
+ edge_orient_hist = np.zeros(hist_bins, dtype="float")
155
+
156
+ # --- (D) Combine All Features ---
157
+ combined_features = np.concatenate([
158
+ avg_color, # 3 values
159
+ lbp_hist, # e.g., 10 values
160
+ glcm_feats, # e.g., 4 values
161
+ np.array([edge_density]), # 1 value
162
+ edge_orient_hist # 8 values
163
+ ])
164
+
165
+ return {
166
+ 'average_color': avg_color,
167
+ 'lbp_hist': lbp_hist,
168
+ 'glcm_features': glcm_feats,
169
+ 'edge_density': edge_density,
170
+ 'edge_orient_hist': edge_orient_hist,
171
+ 'combined_features': combined_features
172
+ }
173
+
174
+
175
+ # ---------------------------------------------------------------------
176
+ # 5. Example Usage
177
+ # ---------------------------------------------------------------------
178
+ if __name__ == "__main__":
179
+ import sys
180
+
181
+ # Provide the path to an image file
182
+ # e.g., python feature_extractor_min.py images/wood_example.jpg
183
+
184
+
185
+ image_path = './wood_patches/patch_012.png'
186
+ image = cv2.imread(image_path, cv2.IMREAD_COLOR)
187
+
188
+ if image is None:
189
+ print(f"Error: Unable to read image at {image_path}")
190
+ else:
191
+ feats = extract_features_from_image(image)
192
+ print("Feature Shapes:")
193
+ for k, v in feats.items():
194
+ if isinstance(v, np.ndarray):
195
+ print(f" {k}: shape={v.shape}")
196
+ else:
197
+ print(f" {k}: {v}")
198
+
199
+ print("\nCombined Feature Vector:")
200
+ print(feats['combined'])
201
+ print("Combined Feature Length:", len(feats['combined']))
svm_model_color.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:421f593486e03e780e4376677331aa39bc65dc7d128152e19f9f6178ad9e4a69
3
+ size 23294