Spaces:
Sleeping
Sleeping
| import pytesseract | |
| import cv2 | |
| import numpy as np | |
| from PIL import Image, ImageDraw, ImageFont | |
| import argparse | |
| class AndroidBottom: | |
| def __init__(self, font_path_medium="Roboto-Medium.ttf", font_path_regular="Roboto-Regular.ttf"): | |
| self.font_path_medium = font_path_medium | |
| self.font_path_regular = font_path_regular | |
| def is_dark_mode(bg_color): | |
| """Deteksi dark mode dari warna background (BGR).""" | |
| r, g, b = float(bg_color[2]), float(bg_color[1]), float(bg_color[0]) | |
| brightness = (r * 299 + g * 587 + b * 114) / 1000 | |
| return brightness < 128 | |
| def extract_anggota_count_from_ocr(self, data): | |
| for i, text in enumerate(data['text']): | |
| if text.lower() in ["anggota", "members"]: | |
| if i > 0 and data['text'][i-1].isdigit(): | |
| return int(data['text'][i-1]) | |
| return 0 | |
| def replace_anggota(self, image, jumlah, font_path=None, show_preview=False, return_theme=False, skip_ocr=False, extracted_data=None, get_ocr_count_only=False): | |
| """Process image and return result with optional parameters""" | |
| try: | |
| # Handle image path or image array | |
| if isinstance(image, str): | |
| # If image is a path, read it | |
| image = cv2.imread(image) | |
| if image is None: | |
| print(f"Error: Tidak dapat membaca gambar {image}") | |
| return None | |
| # Use provided OCR data or perform OCR | |
| if skip_ocr and extracted_data is not None: | |
| data = extracted_data | |
| else: | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| data = pytesseract.image_to_data(gray, output_type=pytesseract.Output.DICT) | |
| if get_ocr_count_only: | |
| return self.extract_anggota_count_from_ocr(data) | |
| h, w = image.shape[:2] | |
| found = False | |
| for i, text in enumerate(data['text']): | |
| if text.lower() in ["anggota", "members"]: | |
| if i > 0 and data['text'][i-1].isdigit(): | |
| found = True | |
| # Tentukan label pengganti sesuai bahasa | |
| label = text if text.lower() in ["anggota", "members"] else "anggota" | |
| replace_text = f"{jumlah} {label}" | |
| x = min(data['left'][i-1], data['left'][i]) | |
| y = min(data['top'][i-1], data['top'][i]) | |
| w_box = data['width'][i-1] + data['width'][i] | |
| h_box = max(data['height'][i-1], data['height'][i]) | |
| margin = 10 | |
| # Ambil warna background dari 10% ke kanan dari posisi anggota | |
| anggota_right = x + w_box | |
| bg_x = int(anggota_right + (w * 0.1)) # 10% ke kanan dari anggota | |
| bg_x = min(bg_x, w - 1) # Pastikan tidak melebihi lebar gambar | |
| bg_color = image[y + h_box//2, bg_x] # Ambil dari tengah tinggi anggota | |
| dark_mode = self.is_dark_mode(bg_color) | |
| theme = 'Dark Mode' if dark_mode else 'Light Mode' | |
| if dark_mode: | |
| rect_color = tuple(int(x) for x in bg_color) | |
| text_color = (88,92,98,255) | |
| else: | |
| rect_color = tuple(int(x) for x in bg_color) | |
| text_color = (90, 94, 95, 255) | |
| # Deteksi sederhana ketebalan font (bold/regular) | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| roi = gray[y:y+h_box, x:x+w_box] | |
| _, binary = cv2.threshold(roi, 180, 255, cv2.THRESH_BINARY_INV) | |
| black_ratio = np.sum(binary == 255) / (w_box * h_box) | |
| print(f"Rasio pixel hitam di area: {black_ratio:.2f}") | |
| if black_ratio > 0.25: | |
| print("Kemungkinan besar: Bold") | |
| auto_font_path = self.font_path_medium | |
| else: | |
| print("Kemungkinan besar: Regular/Tipis") | |
| auto_font_path = self.font_path_regular | |
| # Jika user tidak override font, pakai auto_font_path | |
| effective_font_path = font_path if font_path not in [None, "", "auto"] else auto_font_path | |
| # Pastikan effective_font_path adalah string path | |
| if not isinstance(effective_font_path, str) or not effective_font_path: | |
| effective_font_path = auto_font_path | |
| extra_margin_right = 20 # misal 20px ke kanan | |
| cv2.rectangle( | |
| image, | |
| (x - margin, y - margin), | |
| (x + w_box + margin + extra_margin_right, y + h_box + margin), | |
| rect_color, | |
| -1 | |
| ) | |
| # Tulis teks pengganti dengan PIL agar font bisa custom | |
| image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) | |
| draw = ImageDraw.Draw(image_pil) | |
| try: | |
| font = ImageFont.truetype(effective_font_path, int(h_box * 1.2)) | |
| except Exception as e: | |
| print(f"Font error: {e}, fallback ke default font.") | |
| font = ImageFont.load_default() | |
| bbox = draw.textbbox((0, 0), replace_text, font=font) | |
| text_width = bbox[2] - bbox[0] | |
| text_height = bbox[3] - bbox[1] | |
| # Geser teks ke kanan juga | |
| text_x = data['left'][i-1] | |
| text_y = data['top'][i-1] + (data['height'][i-1] - text_height) // 2 | |
| draw.text((text_x, text_y), replace_text, font=font, fill=text_color) | |
| image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) | |
| break | |
| if not found: | |
| print("Tidak ditemukan pasangan angka + 'anggota'/'members'.") | |
| return None | |
| # cv2.imwrite('output.png', image) | |
| # if show_preview: | |
| # cv2.imshow('Preview (Tekan q untuk keluar)', image) | |
| # while True: | |
| # key = cv2.waitKey(1) & 0xFF | |
| # if key == ord('q'): | |
| # break | |
| # cv2.destroyAllWindows() | |
| result = image if not return_theme else (image, theme) | |
| return result | |
| except Exception as e: | |
| print(f"Error in replace_anggota: {str(e)}") | |
| return None | |
| class IPhoneBottom: | |
| def __init__(self, font_path_medium="SFUIText-Bold.otf", font_path_regular="SFUIText-Semibold.otf"): | |
| self.font_path_medium = font_path_medium | |
| self.font_path_regular = font_path_regular | |
| def is_dark_mode(bg_color): | |
| """Deteksi dark mode dari warna background (BGR).""" | |
| r, g, b = float(bg_color[2]), float(bg_color[1]), float(bg_color[0]) | |
| brightness = (r * 299 + g * 587 + b * 114) / 1000 | |
| return brightness < 128 | |
| def extract_anggota_count_from_ocr(self, data): | |
| for i, text in enumerate(data['text']): | |
| if text.lower() in ["anggota", "members"]: | |
| if i > 0 and data['text'][i-1].isdigit(): | |
| return int(data['text'][i-1]) | |
| return 0 | |
| def replace_anggota(self, image, jumlah, font_path=None, show_preview=False, return_theme=False, skip_ocr=False, extracted_data=None, get_ocr_count_only=False): | |
| """Process image and return result with optional parameters""" | |
| try: | |
| # Handle image path or image array | |
| if isinstance(image, str): | |
| # If image is a path, read it | |
| image = cv2.imread(image) | |
| if image is None: | |
| print(f"Error: Tidak dapat membaca gambar {image}") | |
| return None | |
| # Use provided OCR data or perform OCR | |
| if skip_ocr and extracted_data is not None: | |
| data = extracted_data | |
| else: | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| scale = 2 | |
| processing = cv2.resize(gray, (gray.shape[1]*scale, gray.shape[0]*scale), interpolation=cv2.INTER_CUBIC) | |
| data = pytesseract.image_to_data(processing, output_type=pytesseract.Output.DICT) | |
| if get_ocr_count_only: | |
| return self.extract_anggota_count_from_ocr(data) | |
| h, w = image.shape[:2] | |
| found = False | |
| for i, text in enumerate(data['text']): | |
| if text.lower() in ["anggota", "members"]: | |
| if i > 0 and data['text'][i-1].isdigit(): | |
| found = True | |
| # Tentukan label pengganti sesuai bahasa | |
| label = text if text.lower() in ["anggota", "members"] else "anggota" | |
| replace_text = f"{jumlah} {label}" | |
| # Konversi koordinat hasil OCR ke gambar asli | |
| if extracted_data is None: | |
| scale = 2 | |
| x = int(min(data['left'][i-1], data['left'][i]) / scale) | |
| y = int(min(data['top'][i-1], data['top'][i]) / scale) | |
| w_box = int((data['width'][i-1] + data['width'][i]) / scale) | |
| h_box = int(max(data['height'][i-1], data['height'][i]) / scale) | |
| else: | |
| x = min(data['left'][i-1], data['left'][i]) | |
| y = min(data['top'][i-1], data['top'][i]) | |
| w_box = data['width'][i-1] + data['width'][i] | |
| h_box = max(data['height'][i-1], data['height'][i]) | |
| margin = 10 | |
| # Ambil warna background dari 10% ke kanan dari posisi anggota | |
| anggota_right = x + w_box | |
| bg_x = int(anggota_right + (w * 0.1)) # 10% ke kanan dari anggota | |
| bg_x = min(bg_x, w - 1) # Pastikan tidak melebihi lebar gambar | |
| bg_color = image[y + h_box//2, bg_x] # Ambil dari tengah tinggi anggota | |
| dark_mode = self.is_dark_mode(bg_color) | |
| theme = 'Dark Mode' if dark_mode else 'Light Mode' | |
| if dark_mode: | |
| print("MANTAP MEN") | |
| rect_color = (10, 10, 10, 255) | |
| text_color = (244, 244, 244, 255) | |
| else: | |
| rect_color = tuple(int(x) for x in bg_color) | |
| text_color = (0, 0, 0, 255) | |
| # Deteksi sederhana ketebalan font (bold/regular) | |
| gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) | |
| roi = gray[y:y+h_box, x:x+w_box] | |
| _, binary = cv2.threshold(roi, 180, 255, cv2.THRESH_BINARY_INV) | |
| black_ratio = np.sum(binary == 255) / (w_box * h_box) | |
| if black_ratio > 0.5: | |
| print("Bold") | |
| auto_font_path = self.font_path_medium | |
| else: | |
| print("Regular") | |
| auto_font_path = self.font_path_regular | |
| # Jika user tidak override font, pakai auto_font_path | |
| effective_font_path = font_path if font_path not in [None, "", "auto"] else auto_font_path | |
| # Pastikan effective_font_path adalah string path | |
| if not isinstance(effective_font_path, str) or not effective_font_path: | |
| effective_font_path = auto_font_path | |
| extra_margin_right = 20 | |
| cv2.rectangle( | |
| image, | |
| (x - margin, y - margin), | |
| (x + w_box + margin + extra_margin_right, y + h_box + margin), | |
| rect_color, | |
| -1 | |
| ) | |
| # Tulis teks pengganti dengan PIL agar font bisa custom | |
| image_pil = Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) | |
| draw = ImageDraw.Draw(image_pil) | |
| try: | |
| font = ImageFont.truetype(effective_font_path, int(h_box * 1.2)) | |
| except Exception as e: | |
| print(f"Font error: {e}, fallback ke default font.") | |
| font = ImageFont.load_default() | |
| bbox = draw.textbbox((0, 0), replace_text, font=font) | |
| text_width = bbox[2] - bbox[0] | |
| text_height = bbox[3] - bbox[1] | |
| text_x = x | |
| text_y = y + (h_box - text_height) // 2 | |
| draw.text((text_x, text_y), replace_text, font=font, fill=text_color) | |
| image = cv2.cvtColor(np.array(image_pil), cv2.COLOR_RGB2BGR) | |
| break | |
| if not found: | |
| print("Tidak ditemukan pasangan angka + 'anggota'/'members'.") | |
| return None | |
| # cv2.imwrite('output.png', image) | |
| # if show_preview: | |
| # cv2.imshow('Preview (Tekan q untuk keluar)', image) | |
| # while True: | |
| # key = cv2.waitKey(1) & 0xFF | |
| # if key == ord('q'): | |
| # break | |
| # cv2.destroyAllWindows() | |
| result = image if not return_theme else (image, theme) | |
| return result | |
| except Exception as e: | |
| print(f"Error in replace_anggota: {str(e)}") | |
| return None | |
| if __name__ == "__main__": | |
| parser = argparse.ArgumentParser(description="OCR dan replace jumlah anggota/members dengan angka custom.") | |
| parser.add_argument("image_path", help="Path ke file gambar") | |
| parser.add_argument("jumlah", help="Jumlah anggota/members yang diinginkan (angka saja)") | |
| parser.add_argument("--font", default=None, help="Path font TTF (override, opsional)") | |
| args = parser.parse_args() | |
| replacer = IPhoneBottom() | |
| replacer.replace_anggota(args.image_path, args.jumlah, args.font) |