radiant_ai / services /studio.py
arshvir's picture
Fix indentation after NBSP cleanup in app.py
073316d
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