import cv2 import numpy as np class StudioService: """ Handles standard image editing operations. CRITICAL: Always expects the ORIGINAL image as input to prevent double-processing. """ def process_request(self, image_path, output_path, options): try: img = cv2.imread(image_path) if img is None: raise ValueError("Could not load image.") # 1. Geometry (Rotate/Flip) # Note: We apply this first so filters apply to the correct orientation img = self._apply_geometry(img, options) # 2. Adjustments (Brightness/Contrast/Sharpness) img = self._apply_adjustments(img, options) # 3. Filters filter_type = options.get('filter', 'none') if filter_type != 'none': img = self._apply_filter(img, filter_type) cv2.imwrite(output_path, img) return True, output_path except Exception as e: print(f"Studio Error: {e}") return False, str(e) def _apply_geometry(self, img, options): rotation = int(options.get('rotation', 0)) % 360 if rotation == 90: img = cv2.rotate(img, cv2.ROTATE_90_CLOCKWISE) elif rotation == 180: img = cv2.rotate(img, cv2.ROTATE_180) elif rotation == 270: img = cv2.rotate(img, cv2.ROTATE_90_COUNTERCLOCKWISE) if options.get('flip_h'): img = cv2.flip(img, 1) if options.get('flip_v'): img = cv2.flip(img, 0) return img def _apply_adjustments(self, img, options): # Brightness & Contrast bright = int(options.get('brightness', 0)) contrast = int(options.get('contrast', 0)) if bright != 0 or contrast != 0: if contrast == -100: alpha = 0.0 else: alpha = 131 * (contrast + 127) / (127 * (131 - contrast)) beta = bright img = cv2.convertScaleAbs(img, alpha=alpha, beta=beta) # Saturation sat = int(options.get('saturation', 0)) if sat != 0: # Convert to HSV, handling potential grayscale images if len(img.shape) == 2: img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV).astype("float32") (h, s, v) = cv2.split(hsv) s = s * (1 + sat / 100.0) s = np.clip(s, 0, 255) hsv = cv2.merge([h, s, v]) img = cv2.cvtColor(hsv.astype("uint8"), cv2.COLOR_HSV2BGR) # Sharpness (Unsharp Masking) sharp = int(options.get('sharpness', 0)) if sharp != 0: # Sigma=3.0, Threshold=0 gaussian = cv2.GaussianBlur(img, (0, 0), 3.0) amount = sharp / 50.0 # Scale 0-100 to 0-2.0 strength img = cv2.addWeighted(img, 1 + amount, gaussian, -amount, 0) return img def _apply_filter(self, img, f_type): # Ensure image is 3-channel for filters if len(img.shape) == 2: img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) if f_type == 'grayscale': return cv2.cvtColor(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY), cv2.COLOR_GRAY2BGR) elif f_type == 'sepia': kernel = np.array([[0.272, 0.534, 0.131], [0.349, 0.686, 0.168], [0.393, 0.769, 0.189]]) return cv2.transform(img, kernel) elif f_type == 'blur': return cv2.GaussianBlur(img, (15, 15), 0) elif f_type == 'negative': return cv2.bitwise_not(img) elif f_type == 'cyberpunk': img = img.astype(np.float32) b, g, r = cv2.split(img) b = np.clip(b * 1.2 + 10, 0, 255) r = np.clip(r * 1.1 + 10, 0, 255) return cv2.merge([b, g, r]).astype(np.uint8) elif f_type == 'sketch': gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) inv = cv2.bitwise_not(gray) blur = cv2.GaussianBlur(inv, (21, 21), 0) return cv2.cvtColor(cv2.divide(gray, 255 - blur, scale=256), cv2.COLOR_GRAY2BGR) elif f_type == 'cartoon': gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) gray = cv2.medianBlur(gray, 5) edges = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C, cv2.THRESH_BINARY, 9, 9) color = cv2.bilateralFilter(img, 9, 300, 300) return cv2.bitwise_and(color, cv2.cvtColor(edges, cv2.COLOR_GRAY2BGR)) # Fallback for others (summer, winter, etc) or return original return img