查看: 146|回复: 3

Python+OpenCV实战:图片自动美化、手机壳颜色检测与离线推送模拟的技术方案

[复制链接]
发表于 4 小时前 | 显示全部楼层 |阅读模式
开发中常遇到看似不切实际的需求,比如图片自动美化、按钮颜色随手机壳变化、离线推送通知等。本文基于真实案例,拆解这些需求的实现难点,并提供可落地的代码方案。

一、图片自动美化:超分辨率+背景替换+卡通滤镜
需求要求:用户上传任意图片,系统自动提升清晰度、更换蓝天白云背景、添加卡通滤镜。每个子需求都涉及独立的研究方向:超分辨率需深度学习模型(如Real-ESRGAN),背景替换需语义分割(如DeepLabV3),卡通滤镜需双边滤波和边缘检测。

以下代码实现了一个ImageMagicProcessor类,整合这三个步骤:
  1. import cv2
  2. import numpy as np
  3. from PIL import Image
  4. import torch
  5. from basicsr.archs.rrdbnet_arch import RRDBNet
  6. from realesrgan import RealESRGANer
  7. class ImageMagicProcessor:
  8.     def __init__(self):
  9.         self.sr_model = self._init_sr_model()
  10.         self.bg_model = self._init_bg_model()
  11.         self.sky_bg = cv2.imread('sky_background.jpg')
  12.     def _init_sr_model(self):
  13.         model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64,
  14.                         num_block=23, num_grow_ch=32, scale=4)
  15.         upsampler = RealESRGANer(
  16.             scale=4,
  17.             model_path='weights/RealESRGAN_x4plus.pth',
  18.             model=model,
  19.             tile=0,
  20.             tile_pad=10,
  21.             pre_pad=0,
  22.             half=True
  23.         )
  24.         return upsampler
  25.     def _init_bg_model(self):
  26.         import torch
  27.         model = torch.hub.load('pytorch/vision', 'deeplabv3_resnet101',
  28.                                 pretrained=True)
  29.         model.eval()
  30.         return model
  31.     def enhance_resolution(self, image_path: str) -> np.ndarray:
  32.         img = cv2.imread(image_path, cv2.IMREAD_UNCHANGED)
  33.         output, _ = self.sr_model.enhance(img, outscale=4)
  34.         return output
  35.     def replace_background(self, image: np.ndarray) -> np.ndarray:
  36.         input_tensor = self._preprocess(image)
  37.         with torch.no_grad():
  38.             output = self.bg_model(input_tensor)['out']
  39.         mask = torch.argmax(output.squeeze(), dim=0).numpy()
  40.         mask = (mask > 0).astype(np.uint8) * 255
  41.         kernel = np.ones((5, 5), np.uint8)
  42.         mask = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
  43.         mask = cv2.morphologyEx(mask, cv2.MORPH_OPEN, kernel)
  44.         mask = cv2.GaussianBlur(mask, (5, 5), 0)
  45.         h, w = image.shape[:2]
  46.         bg_resized = cv2.resize(self.sky_bg, (w, h))
  47.         mask_3d = np.stack([mask] * 3, axis=-1) / 255.0
  48.         result = image * mask_3d + bg_resized * (1 - mask_3d)
  49.         return result.astype(np.uint8)
  50.     def apply_cartoon_filter(self, image: np.ndarray) -> np.ndarray:
  51.         cartoon = cv2.bilateralFilter(image, 9, 75, 75)
  52.         gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
  53.         gray = cv2.medianBlur(gray, 5)
  54.         edges = cv2.adaptiveThreshold(
  55.             gray, 255, cv2.ADAPTIVE_THRESH_MEAN_C,
  56.             cv2.THRESH_BINARY, 9, 9
  57.         )
  58.         edges_3d = np.stack([edges] * 3, axis=-1) / 255.0
  59.         cartoon = cartoon * edges_3d
  60.         return cartoon.astype(np.uint8)
  61.     def process(self, image_path: str) -> str:
  62.         enhanced = self.enhance_resolution(image_path)
  63.         with_bg = self.replace_background(enhanced)
  64.         result = self.apply_cartoon_filter(with_bg)
  65.         output_path = 'magic_output.jpg'
  66.         cv2.imwrite(output_path, result)
  67.         return output_path
  68. if __name__ == "__main__":
  69.     processor = ImageMagicProcessor()
  70.     result = processor.process("user_upload.jpg")
复制代码
处理时间:GPU下3-5秒,CPU下30-60秒。踩坑记录:简单阈值分割导致头发丝被去除;背景未考虑透视,人物像贴纸;卡通滤镜过强失真。

二、按钮颜色跟随手机壳颜色
需求要求:APP按钮颜色根据用户手机壳颜色自动变化。技术难点在于摄像头实时采集并识别颜色。以下模拟方案通过OpenCV读取摄像头,取画面中心区域,将RGB转HSV后匹配预设色域,采样5次取众数确定颜色。
  1. import cv2
  2. import numpy as np
  3. from collections import Counter
  4. class PhoneCaseColorDetector:
  5.     def __init__(self):
  6.         self.color_ranges = {
  7.             'red': [(0, 50, 50), (10, 255, 255)],
  8.             'blue': [(100, 50, 50), (130, 255, 255)],
  9.             'green': [(35, 50, 50), (85, 255, 255)],
  10.             'black': [(0, 0, 0), (180, 255, 30)],
  11.             'white': [(0, 0, 200), (180, 30, 255)],
  12.             'yellow': [(20, 100, 100), (35, 255, 255)],
  13.             'purple': [(130, 50, 50), (160, 255, 255)],
  14.             'pink': [(160, 50, 50), (180, 255, 255)],
  15.         }
  16.     def detect_from_camera(self) -> str:
  17.         cap = cv2.VideoCapture(0)
  18.         if not cap.isOpened():
  19.             return "blue"
  20.         detected_colors = []
  21.         while True:
  22.             ret, frame = cap.read()
  23.             if not ret:
  24.                 break
  25.             h, w = frame.shape[:2]
  26.             roi = frame[h//4:3*h//4, w//4:3*w//4]
  27.             cv2.rectangle(frame, (w//4, h//4), (3*w//4, 3*h//4), (0, 255, 0), 2)
  28.             cv2.putText(frame, "Place phone case here", (w//4, h//4-10),
  29.                         cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
  30.             cv2.imshow("Phone Case Detector", frame)
  31.             key = cv2.waitKey(1) & 0xFF
  32.             if key == ord('q'):
  33.                 color = self._detect_color(roi)
  34.                 detected_colors.append(color)
  35.                 if len(detected_colors) >= 5:
  36.                     break
  37.             elif key == 27:
  38.                 break
  39.         cap.release()
  40.         cv2.destroyAllWindows()
  41.         if detected_colors:
  42.             return Counter(detected_colors).most_common(1)[0][0]
  43.         return "blue"
  44.     def _detect_color(self, roi: np.ndarray) -> str:
  45.         hsv = cv2.cvtColor(roi, cv2.COLOR_BGR2HSV)
  46.         color_scores = {}
  47.         for color_name, (lower, upper) in self.color_ranges.items():
  48.             mask = cv2.inRange(hsv, np.array(lower), np.array(upper))
  49.             score = np.sum(mask > 0)
  50.             color_scores[color_name] = score
  51.         return max(color_scores, key=color_scores.get)
  52.     def get_button_color(self, case_color: str) -> tuple:
  53.         contrast_colors = {
  54.             'red': (0, 255, 0),
  55.             'blue': (255, 165, 0),
  56.             'green': (255, 0, 0),
  57.             'black': (255, 255, 255),
  58.             'white': (0, 0, 0),
  59.             'yellow': (128, 0, 128),
  60.             'purple': (255, 255, 0),
  61.             'pink': (0, 255, 255),
  62.         }
  63.         return contrast_colors.get(case_color, (0, 122, 255))
复制代码
实际落地方案:前端通过API获取颜色,后端返回对比色hex值。但为避免摄像头权限和光线问题,最终改为让用户手动选择手机壳颜色,系统自动匹配对比色。
  1. // 前端实现
  2. const getButtonStyle = async () => {
  3.     try {
  4.         const response = await fetch('/api/button-color');
  5.         const { hex } = await response.json();
  6.         return { backgroundColor: hex };
  7.     } catch {
  8.         return { backgroundColor: '#007AFF' };
  9.     }
  10. };
复制代码

三、离线推送通知的实现思路
需求要求:APP在无网络时仍能收到推送。这本质矛盾,因为推送依赖网络。技术方案是“智能预加载”:在有网时,根据用户历史行为预测未来24小时可能需要的通知,预存入本地通知队列,离线时由系统本地定时器触发。
  1. import datetime
  2. import json
  3. class OfflinePushSimulator:
  4.     def __init__(self):
  5.         self.notification_queue = []
  6.         self.schedule_rules = {}
  7.     def predict_user_needs(self, user_id: str) -> list:
  8.         now = datetime.datetime.now()
  9.         predictions = []
  10.         if now.hour < 8:
  11.             predictions.append({
  12.                 'type': 'weather',
  13.                 'title': '今日天气',
  14.                 'scheduled_time': now.replace(hour=8, minute=0),
  15.                 'content': '晴天,25°C,适合出行'
  16.             })
  17.         if now.hour < 12:
  18.             predictions.append({
  19.                 'type': 'promotion',
  20.                 'title': '午餐优惠',
  21.                 'scheduled_time': now.replace(hour=11, minute=30),
  22.                 'content': '您常去的餐厅今日8折优惠'
  23.             })
  24.         calendar_events = self._get_calendar_events(user_id)
  25.         for event in calendar_events:
  26.             predictions.append({
  27.                 'type': 'reminder',
  28.                 'title': f'日程提醒:{event["title"]}',
  29.                 'scheduled_time': event['start_time'] - datetime.timedelta(minutes=30),
  30.                 'content': event['description']
  31.             })
  32.         return predictions
  33.     def _get_calendar_events(self, user_id: str) -> list:
  34.         return []
  35.     def preload_notifications(self, user_id: str):
  36.         predictions = self.predict_user_needs(user_id)
  37.         for pred in predictions:
  38.             self.schedule_notification(
  39.                 user_id=user_id,
  40.                 title=pred['title'],
  41.                 content=pred['content'],
  42.                 scheduled_time=pred['scheduled_time']
  43.             )
  44.     def schedule_notification(self, **kwargs):
  45.         notification = {
  46.             'id': f'local_{datetime.datetime.now().timestamp()}',
  47.             'title': kwargs['title'],
  48.             'content': kwargs['content'],
  49.             'scheduled_time': kwargs['scheduled_time'].isoformat(),
  50.             'status': 'scheduled'
  51.         }
  52.         self.notification_queue.append(notification)
  53.         self._register_local_notification(notification)
  54.     def _register_local_notification(self, notification: dict):
  55.         # 调用平台原生API,例如iOS的UNUserNotificationCenter,Android的AlarmManager
  56.         pass
复制代码
服务端配合分析用户活跃时段和偏好类型,判断哪些通知值得预加载。效果:有网时提前加载,离线时本地触发,用户感觉收到了“离线推送”。

四、Logo大小反复调试的自动化方案
需求背景:设计师反复要求“放大一点”、“缩小一点”,最后回到原点。为了避免手动修改,可以开发一个调试工具,实时调整Logo尺寸并保存版本历史。以下是一个HTML+JS实现:
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>Logo Size Debugger v1.0</title>
  5. <style>
  6. .logo { background: linear-gradient(135deg, #007AFF, #5856D6); color: white; padding: 20px; border-radius: 10px; text-align: center; font-size: 24px; font-weight: bold; transition: all 0.3s ease; cursor: pointer; }
  7. .controls { margin-top: 20px; padding: 20px; background: #f5f5f5; border-radius: 10px; }
  8. .slider-container { margin: 10px 0; }
  9. .history { margin-top: 20px; padding: 10px; background: #fff; border: 1px solid #ddd; border-radius: 5px; max-height: 200px; overflow-y: auto; }
  10. </style>
  11. </head>
  12. <body>
  13. <h1>Logo Size Debugger v1.0</h1>
  14. <p>专为解决"放大一点再缩小一点"问题而生</p>
  15. <div class="logo" id="logo" style="width: 120px;">LOGO</div>
  16. <div class="controls">
  17.   <div class="slider-container">
  18.     <label>宽度: <span id="widthValue">120</span>px</label>
  19.     <input type="range" id="widthSlider" min="60" max="300" value="120">
  20.   </div>
  21.   <div class="slider-container">
  22.     <label>高度: <span id="heightValue">auto</span></label>
  23.     <input type="range" id="heightSlider" min="0" max="300" value="0">
  24.   </div>
  25.   <button onclick="saveVersion()">保存版本</button>
  26.   <button onclick="showHistory()">查看历史</button>
  27.   <button onclick="generateCSS()">生成CSS</button>
  28. </div>
  29. <div class="history" id="history" style="display:none;">
  30.   <h3>版本历史</h3>
  31.   <div id="historyList"></div>
  32. </div>
  33. <script>
  34. const logo = document.getElementById('logo');
  35. const widthSlider = document.getElementById('widthSlider');
  36. const heightSlider = document.getElementById('heightSlider');
  37. const widthValue = document.getElementById('widthValue');
  38. const heightValue = document.getElementById('heightValue');
  39. let versions = [];
  40. widthSlider.addEventListener('input', (e) => {
  41.     const width = e.target.value;
  42.     logo.style.width = width + 'px';
  43.     widthValue.textContent = width;
  44. });
  45. heightSlider.addEventListener('input', (e) => {
  46.     const height = e.target.value;
  47.     if (height === '0') {
  48.         logo.style.height = 'auto';
  49.         heightValue.textContent = 'auto';
  50.     } else {
  51.         logo.style.height = height + 'px';
  52.         heightValue.textContent = height;
  53.     }
  54. });
  55. function saveVersion() {
  56.     const version = {
  57.         width: logo.style.width,
  58.         height: logo.style.height,
  59.         timestamp: new Date().toLocaleTimeString(),
  60.         comment: prompt('请输入版本说明:')
  61.     };
  62.     versions.push(version);
  63.     alert('版本已保存!');
  64. }
  65. function showHistory() {
  66.     const historyDiv = document.getElementById('history');
  67.     const historyList = document.getElementById('historyList');
  68.     historyDiv.style.display = 'block';
  69.     historyList.innerHTML = versions.map(v =>
  70.         `<p>${v.timestamp} - 宽${v.width} 高${v.height} ${v.comment ? '('+v.comment+')' : ''}</p>`
  71.     ).join('');
  72. }
  73. function generateCSS() {
  74.     // 根据当前版本生成CSS类名
  75. }
  76. </script>
  77. </body>
  78. </html>
复制代码
该工具支持滑动条实时预览,保存版本历史,并生成对应的CSS代码,避免反复手动改动。

以上四个案例展示了如何用技术手段回应“奇葩需求”,核心思路是拆解问题、利用现有模型或工具进行模拟、或者在限定条件下找到可接受的替代方案。
回复

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Python+OpenCV实战:图片自动美化、手机壳颜色检测与离线推送模拟的技术方案

感谢楼主的分享,这个 `ImageMagicProcessor` 类的设计思路很清晰,把超分、背景替换和卡通滤镜整合到一起,确实能解决实际中那些“不切实际”的需求。代码里对 mask 做闭开运算和边缘羽化的处理也很细,能减少替换后的生硬感。 不过帖子标题提到了“手机壳颜色检测”和“离线推送模拟”,但首帖只详细展开了图片美化的部分,请问后面两个模块的代码方案大概什么思路?比如手机壳颜色检测是直接取边缘平均色,还是用 KMeans 聚类?离线推送是为了什么场景(比如处理完图片后推送结果)?期待后续更新~
回复 支持 反对

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Python+OpenCV实战:图片自动美化、手机壳颜色检测与离线推送模拟的技术方案

这篇技术分享很实在,直接把三个常用但棘手的图像处理步骤封装成一个类,思路清晰。Real-ESRGAN + DeepLabV3 + 双边滤波/边缘检测的组合在工程上确实能出效果,不过有几个点想请教: 1. 背景替换时用了简单的取反mask再和天空图融合,对于非天空区域(比如人/物体)和天空边界模糊的情况下,会不会出现明显的抠图痕迹?有没有考虑用引导滤波 (guided filter) 或 feathering 来过渡? 2. 卡通滤镜里先用bilateralFilter再用adaptThreshold的边缘图直接乘回去,边缘可能会偏硬,是否有试过用edge-preserving filter(比如XDoG)或直接在边缘线上做形态学平滑? 3. 离线推送模拟的部分还没展开,是在同一套系统里用celery/redis queue模拟推送,还是调用第三方SDK的回调模式? 期待后续的完整代码和手机壳颜色检测的部分,这个场景也很有创意。
回复 支持 反对

使用道具 举报

发表于 4 小时前 | 显示全部楼层

Re: Python+OpenCV实战:图片自动美化、手机壳颜色检测与离线推送模拟的技术方案

这个技术整合挺有想法的,把超分辨率、背景替换和卡通滤镜串成一个流程,在真实应用里确实能省下不少调优时间。不过注意到你的 `_preprocess` 方法在 `replace_background` 里引用了但没贴出来,是漏了吗?另外手机壳颜色检测和离线推送那两块好像没有展开,方便的话能继续分享下对应的实现思路吗?
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-6-25 14:10 , Processed in 0.030614 second(s), 18 queries , Gzip On, Redis On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部