|
a |
|
b/gi-bleeding-detection.py |
|
|
1 |
import numpy as np |
|
|
2 |
import cv2 |
|
|
3 |
import tkinter as tk |
|
|
4 |
from tkinter import filedialog, messagebox |
|
|
5 |
from PIL import Image, ImageTk |
|
|
6 |
import os |
|
|
7 |
from sklearn.cluster import KMeans |
|
|
8 |
import matplotlib.pyplot as plt |
|
|
9 |
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg |
|
|
10 |
|
|
|
11 |
class GIBleedingDetector: |
|
|
12 |
def __init__(self): |
|
|
13 |
self.original_image = None |
|
|
14 |
self.processed_image = None |
|
|
15 |
self.mask = None |
|
|
16 |
self.bleeding_percentage = 0 |
|
|
17 |
self.bleeding_status = "No image analyzed" |
|
|
18 |
|
|
|
19 |
def load_image(self, image_path): |
|
|
20 |
"""Load and prepare the image for processing""" |
|
|
21 |
self.original_image = cv2.imread(image_path) |
|
|
22 |
if self.original_image is None: |
|
|
23 |
return False |
|
|
24 |
return True |
|
|
25 |
|
|
|
26 |
def process_image(self): |
|
|
27 |
"""Process the image to detect GI bleeding""" |
|
|
28 |
if self.original_image is None: |
|
|
29 |
return False |
|
|
30 |
|
|
|
31 |
# Convert to RGB for better color analysis |
|
|
32 |
image_rgb = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2RGB) |
|
|
33 |
|
|
|
34 |
# Resize image for faster processing if needed |
|
|
35 |
resized = cv2.resize(image_rgb, (0, 0), fx=0.5, fy=0.5) if image_rgb.shape[0] > 1000 else image_rgb |
|
|
36 |
|
|
|
37 |
# Reshape the image for K-means |
|
|
38 |
pixels = resized.reshape(-1, 3).astype(np.float32) |
|
|
39 |
|
|
|
40 |
# Define criteria and apply K-means |
|
|
41 |
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0) |
|
|
42 |
k = 5 # Number of clusters |
|
|
43 |
_, labels, centers = cv2.kmeans(pixels, k, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS) |
|
|
44 |
|
|
|
45 |
# Convert back to uint8 |
|
|
46 |
centers = np.uint8(centers) |
|
|
47 |
segmented_image = centers[labels.flatten()] |
|
|
48 |
segmented_image = segmented_image.reshape(resized.shape) |
|
|
49 |
|
|
|
50 |
# Detect red regions (potential bleeding) |
|
|
51 |
# Convert to HSV for better color segmentation |
|
|
52 |
hsv_img = cv2.cvtColor(segmented_image, cv2.COLOR_RGB2HSV) |
|
|
53 |
|
|
|
54 |
# Define range for red color in HSV |
|
|
55 |
lower_red1 = np.array([0, 120, 70]) |
|
|
56 |
upper_red1 = np.array([10, 255, 255]) |
|
|
57 |
lower_red2 = np.array([170, 120, 70]) |
|
|
58 |
upper_red2 = np.array([180, 255, 255]) |
|
|
59 |
|
|
|
60 |
# Create masks for red regions |
|
|
61 |
mask1 = cv2.inRange(hsv_img, lower_red1, upper_red1) |
|
|
62 |
mask2 = cv2.inRange(hsv_img, lower_red2, upper_red2) |
|
|
63 |
|
|
|
64 |
# Combine masks |
|
|
65 |
self.mask = cv2.bitwise_or(mask1, mask2) |
|
|
66 |
|
|
|
67 |
# Calculate bleeding percentage |
|
|
68 |
total_pixels = self.mask.size |
|
|
69 |
bleeding_pixels = cv2.countNonZero(self.mask) |
|
|
70 |
self.bleeding_percentage = (bleeding_pixels / total_pixels) * 100 |
|
|
71 |
|
|
|
72 |
# Determine bleeding status |
|
|
73 |
if self.bleeding_percentage > 5: |
|
|
74 |
self.bleeding_status = "High probability of bleeding" |
|
|
75 |
elif self.bleeding_percentage > 1: |
|
|
76 |
self.bleeding_status = "Moderate probability of bleeding" |
|
|
77 |
else: |
|
|
78 |
self.bleeding_status = "Low probability of bleeding" |
|
|
79 |
|
|
|
80 |
# Create visual result |
|
|
81 |
self.processed_image = cv2.bitwise_and(segmented_image, segmented_image, mask=self.mask) |
|
|
82 |
|
|
|
83 |
# Resize the mask to match the original image size if needed |
|
|
84 |
if resized.shape != image_rgb.shape: |
|
|
85 |
self.mask = cv2.resize(self.mask, (image_rgb.shape[1], image_rgb.shape[0])) |
|
|
86 |
self.processed_image = cv2.resize(self.processed_image, (image_rgb.shape[1], image_rgb.shape[0])) |
|
|
87 |
|
|
|
88 |
return True |
|
|
89 |
|
|
|
90 |
def get_overlay_image(self): |
|
|
91 |
"""Create an overlay of original image with bleeding highlighted""" |
|
|
92 |
if self.original_image is None or self.mask is None: |
|
|
93 |
return None |
|
|
94 |
|
|
|
95 |
# Convert original image to RGB |
|
|
96 |
original_rgb = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2RGB) |
|
|
97 |
|
|
|
98 |
# Create a mask with same dimensions as original |
|
|
99 |
mask_resized = cv2.resize(self.mask, (original_rgb.shape[1], original_rgb.shape[0])) |
|
|
100 |
|
|
|
101 |
# Create overlay |
|
|
102 |
overlay = original_rgb.copy() |
|
|
103 |
overlay[mask_resized > 0] = [255, 0, 0] # Mark bleeding areas in red |
|
|
104 |
|
|
|
105 |
# Create blended image - 70% original, 30% overlay |
|
|
106 |
blended = cv2.addWeighted(original_rgb, 0.7, overlay, 0.3, 0) |
|
|
107 |
|
|
|
108 |
return blended |
|
|
109 |
|
|
|
110 |
def get_analysis_result(self): |
|
|
111 |
"""Return analysis results as a dictionary""" |
|
|
112 |
return { |
|
|
113 |
"bleeding_percentage": round(self.bleeding_percentage, 2), |
|
|
114 |
"bleeding_status": self.bleeding_status |
|
|
115 |
} |
|
|
116 |
|
|
|
117 |
class GIBleedingDetectionApp: |
|
|
118 |
def __init__(self, root): |
|
|
119 |
self.root = root |
|
|
120 |
self.root.title("GI Bleeding Detection Tool") |
|
|
121 |
self.root.geometry("1200x800") |
|
|
122 |
self.root.configure(bg="#f0f0f0") |
|
|
123 |
|
|
|
124 |
self.detector = GIBleedingDetector() |
|
|
125 |
self.create_widgets() |
|
|
126 |
|
|
|
127 |
def create_widgets(self): |
|
|
128 |
# Top frame for buttons |
|
|
129 |
self.top_frame = tk.Frame(self.root, bg="#f0f0f0") |
|
|
130 |
self.top_frame.pack(pady=10, fill=tk.X) |
|
|
131 |
|
|
|
132 |
# Load button |
|
|
133 |
self.load_btn = tk.Button(self.top_frame, text="Load Image", |
|
|
134 |
command=self.load_image, |
|
|
135 |
bg="#4CAF50", fg="white", |
|
|
136 |
font=("Arial", 12), padx=10, pady=5) |
|
|
137 |
self.load_btn.pack(side=tk.LEFT, padx=10) |
|
|
138 |
|
|
|
139 |
# Analyze button |
|
|
140 |
self.analyze_btn = tk.Button(self.top_frame, text="Analyze Image", |
|
|
141 |
command=self.analyze_image, |
|
|
142 |
bg="#2196F3", fg="white", |
|
|
143 |
font=("Arial", 12), padx=10, pady=5, |
|
|
144 |
state=tk.DISABLED) |
|
|
145 |
self.analyze_btn.pack(side=tk.LEFT, padx=10) |
|
|
146 |
|
|
|
147 |
# Save button |
|
|
148 |
self.save_btn = tk.Button(self.top_frame, text="Save Results", |
|
|
149 |
command=self.save_results, |
|
|
150 |
bg="#FFC107", fg="white", |
|
|
151 |
font=("Arial", 12), padx=10, pady=5, |
|
|
152 |
state=tk.DISABLED) |
|
|
153 |
self.save_btn.pack(side=tk.LEFT, padx=10) |
|
|
154 |
|
|
|
155 |
# Main content frame |
|
|
156 |
self.content_frame = tk.Frame(self.root, bg="#f0f0f0") |
|
|
157 |
self.content_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=10) |
|
|
158 |
|
|
|
159 |
# Original image frame |
|
|
160 |
self.original_frame = tk.LabelFrame(self.content_frame, text="Original Image", |
|
|
161 |
bg="#f0f0f0", font=("Arial", 12)) |
|
|
162 |
self.original_frame.grid(row=0, column=0, padx=10, pady=10, sticky=tk.NSEW) |
|
|
163 |
|
|
|
164 |
self.original_canvas = tk.Label(self.original_frame, bg="black") |
|
|
165 |
self.original_canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) |
|
|
166 |
|
|
|
167 |
# Processed image frame |
|
|
168 |
self.processed_frame = tk.LabelFrame(self.content_frame, text="Bleeding Detection Result", |
|
|
169 |
bg="#f0f0f0", font=("Arial", 12)) |
|
|
170 |
self.processed_frame.grid(row=0, column=1, padx=10, pady=10, sticky=tk.NSEW) |
|
|
171 |
|
|
|
172 |
self.processed_canvas = tk.Label(self.processed_frame, bg="black") |
|
|
173 |
self.processed_canvas.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) |
|
|
174 |
|
|
|
175 |
# Results frame |
|
|
176 |
self.results_frame = tk.LabelFrame(self.content_frame, text="Analysis Results", |
|
|
177 |
bg="#f0f0f0", font=("Arial", 12)) |
|
|
178 |
self.results_frame.grid(row=1, column=0, columnspan=2, padx=10, pady=10, sticky=tk.NSEW) |
|
|
179 |
|
|
|
180 |
# Results display |
|
|
181 |
self.results_text = tk.Label(self.results_frame, text="No image analyzed", |
|
|
182 |
font=("Arial", 14), bg="#f0f0f0", justify=tk.LEFT) |
|
|
183 |
self.results_text.pack(side=tk.LEFT, padx=20, pady=20) |
|
|
184 |
|
|
|
185 |
# Graph frame for visualization |
|
|
186 |
self.graph_frame = tk.Frame(self.results_frame, bg="#f0f0f0") |
|
|
187 |
self.graph_frame.pack(side=tk.RIGHT, padx=20, pady=20, fill=tk.BOTH, expand=True) |
|
|
188 |
|
|
|
189 |
# Configure the grid |
|
|
190 |
self.content_frame.grid_columnconfigure(0, weight=1) |
|
|
191 |
self.content_frame.grid_columnconfigure(1, weight=1) |
|
|
192 |
self.content_frame.grid_rowconfigure(0, weight=3) |
|
|
193 |
self.content_frame.grid_rowconfigure(1, weight=1) |
|
|
194 |
|
|
|
195 |
# Status bar |
|
|
196 |
self.status_bar = tk.Label(self.root, text="Ready", bd=1, relief=tk.SUNKEN, anchor=tk.W) |
|
|
197 |
self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) |
|
|
198 |
|
|
|
199 |
def load_image(self): |
|
|
200 |
file_path = filedialog.askopenfilename( |
|
|
201 |
title="Select Image", |
|
|
202 |
filetypes=[("Image files", "*.jpg *.jpeg *.png *.bmp")]) |
|
|
203 |
|
|
|
204 |
if not file_path: |
|
|
205 |
return |
|
|
206 |
|
|
|
207 |
self.status_bar.config(text=f"Loading image: {file_path}") |
|
|
208 |
|
|
|
209 |
if self.detector.load_image(file_path): |
|
|
210 |
# Convert for display |
|
|
211 |
img = cv2.cvtColor(self.detector.original_image, cv2.COLOR_BGR2RGB) |
|
|
212 |
img = self.resize_for_display(img, 500) |
|
|
213 |
|
|
|
214 |
# Display image |
|
|
215 |
self.tk_img = ImageTk.PhotoImage(Image.fromarray(img)) |
|
|
216 |
self.original_canvas.config(image=self.tk_img) |
|
|
217 |
|
|
|
218 |
# Update UI |
|
|
219 |
self.analyze_btn.config(state=tk.NORMAL) |
|
|
220 |
self.results_text.config(text="Image loaded. Click 'Analyze Image' to detect bleeding.") |
|
|
221 |
self.status_bar.config(text=f"Image loaded: {os.path.basename(file_path)}") |
|
|
222 |
else: |
|
|
223 |
messagebox.showerror("Error", "Could not load the image. Please try again with a different file.") |
|
|
224 |
self.status_bar.config(text="Error loading image") |
|
|
225 |
|
|
|
226 |
def analyze_image(self): |
|
|
227 |
if self.detector.original_image is None: |
|
|
228 |
messagebox.showinfo("Info", "Please load an image first.") |
|
|
229 |
return |
|
|
230 |
|
|
|
231 |
self.status_bar.config(text="Analyzing image for GI bleeding...") |
|
|
232 |
|
|
|
233 |
# Process the image |
|
|
234 |
if self.detector.process_image(): |
|
|
235 |
# Get the overlay image with bleeding highlighted |
|
|
236 |
overlay_img = self.detector.get_overlay_image() |
|
|
237 |
overlay_img = self.resize_for_display(overlay_img, 500) |
|
|
238 |
|
|
|
239 |
# Display processed image |
|
|
240 |
self.processed_tk_img = ImageTk.PhotoImage(Image.fromarray(overlay_img)) |
|
|
241 |
self.processed_canvas.config(image=self.processed_tk_img) |
|
|
242 |
|
|
|
243 |
# Get analysis results |
|
|
244 |
results = self.detector.get_analysis_result() |
|
|
245 |
|
|
|
246 |
# Update results text |
|
|
247 |
result_text = f"Bleeding Detection Analysis:\n\n" |
|
|
248 |
result_text += f"Bleeding Area: {results['bleeding_percentage']}% of image\n" |
|
|
249 |
result_text += f"Assessment: {results['bleeding_status']}" |
|
|
250 |
|
|
|
251 |
self.results_text.config(text=result_text) |
|
|
252 |
|
|
|
253 |
# Create visualization graph |
|
|
254 |
self.create_results_graph(results['bleeding_percentage']) |
|
|
255 |
|
|
|
256 |
# Update UI |
|
|
257 |
self.save_btn.config(state=tk.NORMAL) |
|
|
258 |
self.status_bar.config(text="Analysis complete") |
|
|
259 |
else: |
|
|
260 |
messagebox.showerror("Error", "Could not analyze the image. Please try again.") |
|
|
261 |
self.status_bar.config(text="Error during analysis") |
|
|
262 |
|
|
|
263 |
def create_results_graph(self, bleeding_percentage): |
|
|
264 |
# Clear previous graph |
|
|
265 |
for widget in self.graph_frame.winfo_children(): |
|
|
266 |
widget.destroy() |
|
|
267 |
|
|
|
268 |
# Create figure and axis |
|
|
269 |
fig, ax = plt.subplots(figsize=(4, 3)) |
|
|
270 |
|
|
|
271 |
# Data for the gauge chart |
|
|
272 |
categories = ['Healthy', 'Bleeding'] |
|
|
273 |
values = [100 - bleeding_percentage, bleeding_percentage] |
|
|
274 |
colors = ['#4CAF50', '#F44336'] |
|
|
275 |
|
|
|
276 |
# Create the pie chart |
|
|
277 |
ax.pie(values, labels=categories, colors=colors, autopct='%1.1f%%', |
|
|
278 |
startangle=90, wedgeprops={'edgecolor': 'white', 'linewidth': 1}) |
|
|
279 |
ax.set_title("Tissue Analysis", fontsize=12) |
|
|
280 |
|
|
|
281 |
# Add to tkinter window |
|
|
282 |
canvas = FigureCanvasTkAgg(fig, self.graph_frame) |
|
|
283 |
canvas.draw() |
|
|
284 |
canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) |
|
|
285 |
|
|
|
286 |
def resize_for_display(self, image, max_dim): |
|
|
287 |
"""Resize image while maintaining aspect ratio""" |
|
|
288 |
h, w = image.shape[:2] |
|
|
289 |
if h > w: |
|
|
290 |
new_h = max_dim |
|
|
291 |
new_w = int(w * (max_dim / h)) |
|
|
292 |
else: |
|
|
293 |
new_w = max_dim |
|
|
294 |
new_h = int(h * (max_dim / w)) |
|
|
295 |
|
|
|
296 |
return cv2.resize(image, (new_w, new_h)) |
|
|
297 |
|
|
|
298 |
def save_results(self): |
|
|
299 |
if self.detector.processed_image is None: |
|
|
300 |
messagebox.showinfo("Info", "Please analyze an image first.") |
|
|
301 |
return |
|
|
302 |
|
|
|
303 |
# Ask for directory to save results |
|
|
304 |
save_dir = filedialog.askdirectory(title="Select Directory to Save Results") |
|
|
305 |
|
|
|
306 |
if not save_dir: |
|
|
307 |
return |
|
|
308 |
|
|
|
309 |
try: |
|
|
310 |
# Save the overlay image |
|
|
311 |
overlay_img = self.detector.get_overlay_image() |
|
|
312 |
cv2.imwrite(os.path.join(save_dir, "bleeding_detection_result.jpg"), |
|
|
313 |
cv2.cvtColor(overlay_img, cv2.COLOR_RGB2BGR)) |
|
|
314 |
|
|
|
315 |
# Save the analysis results as text |
|
|
316 |
results = self.detector.get_analysis_result() |
|
|
317 |
with open(os.path.join(save_dir, "analysis_results.txt"), "w") as f: |
|
|
318 |
f.write(f"GI Bleeding Detection Analysis Results\n") |
|
|
319 |
f.write(f"=====================================\n\n") |
|
|
320 |
f.write(f"Bleeding Area: {results['bleeding_percentage']}% of image\n") |
|
|
321 |
f.write(f"Assessment: {results['bleeding_status']}\n") |
|
|
322 |
f.write(f"\nAnalysis performed on {os.path.basename(save_dir)}\n") |
|
|
323 |
|
|
|
324 |
messagebox.showinfo("Success", f"Results saved to {save_dir}") |
|
|
325 |
self.status_bar.config(text=f"Results saved to {save_dir}") |
|
|
326 |
|
|
|
327 |
except Exception as e: |
|
|
328 |
messagebox.showerror("Error", f"Could not save results: {str(e)}") |
|
|
329 |
self.status_bar.config(text="Error saving results") |
|
|
330 |
|
|
|
331 |
if __name__ == "__main__": |
|
|
332 |
# Create main window |
|
|
333 |
root = tk.Tk() |
|
|
334 |
app = GIBleedingDetectionApp(root) |
|
|
335 |
|
|
|
336 |
# Run the application |
|
|
337 |
root.mainloop() |