process_image.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388
  1. import os
  2. import uuid
  3. import tempfile
  4. import requests
  5. import base64
  6. import re
  7. from PIL import Image
  8. from flask import Flask, request, jsonify
  9. from lib import Qiniu
  10. import logging
  11. # 配置日志
  12. logging.basicConfig(level=logging.INFO)
  13. logger = logging.getLogger(__name__)
  14. # 尝试导入rembg,如果没有安装则提示
  15. try:
  16. from rembg import remove
  17. REMBG_AVAILABLE = True
  18. except ImportError:
  19. REMBG_AVAILABLE = False
  20. logger.warning("rembg库未安装,请运行: pip install rembg")
  21. def is_base64_image(image_str):
  22. """
  23. 检测字符串是否为base64编码的图片
  24. 参数:
  25. - image_str: 待检测的字符串
  26. 返回:
  27. - bool: True表示是base64图片,False表示不是
  28. """
  29. # 检查是否包含data:image协议头
  30. if image_str.startswith('data:image'):
  31. return True
  32. # 检查是否看起来像base64编码(长度合理且包含base64字符)
  33. # base64字符集:A-Z, a-z, 0-9, +, /, =
  34. if len(image_str) > 100: # base64图片通常很长
  35. # 移除可能的空白字符
  36. cleaned = image_str.strip().replace('\n', '').replace('\r', '')
  37. # 检查是否主要由base64字符组成
  38. base64_pattern = re.compile(r'^[A-Za-z0-9+/]+={0,2}$')
  39. return bool(base64_pattern.match(cleaned))
  40. return False
  41. def save_base64_image(base64_str, output_path):
  42. """
  43. 将base64编码的图片保存到本地文件
  44. 参数:
  45. - base64_str: base64编码的图片字符串(支持带data:image协议头或纯base64)
  46. - output_path: 保存路径
  47. 返回:
  48. - 保存的文件路径
  49. """
  50. try:
  51. # 如果包含data:image协议头,提取base64部分
  52. if base64_str.startswith('data:image'):
  53. # 格式: data:image/png;base64,iVBORw0KG...
  54. base64_data = base64_str.split(',', 1)[1] if ',' in base64_str else base64_str
  55. else:
  56. base64_data = base64_str
  57. # 移除可能的空白字符
  58. base64_data = base64_data.strip().replace('\n', '').replace('\r', '')
  59. # 解码base64
  60. image_data = base64.b64decode(base64_data)
  61. # 保存到文件
  62. with open(output_path, 'wb') as f:
  63. f.write(image_data)
  64. logger.info(f"成功保存base64图片: {output_path}")
  65. return output_path
  66. except Exception as e:
  67. logger.error(f"保存base64图片失败: {e}")
  68. raise Exception(f"保存base64图片失败: {str(e)}")
  69. def download_image_from_url(url, output_path):
  70. """
  71. 从URL下载图片到本地
  72. 参数:
  73. - url: 图片URL
  74. - output_path: 保存路径
  75. 返回:
  76. - 保存的文件路径
  77. """
  78. try:
  79. response = requests.get(url, stream=True, timeout=30)
  80. response.raise_for_status()
  81. # 检查是否为图片类型
  82. content_type = response.headers.get('content-type', '').lower()
  83. if 'image' not in content_type:
  84. logger.warning(f"URL可能不是图片类型: {content_type}")
  85. # 保存图片
  86. with open(output_path, 'wb') as f:
  87. for chunk in response.iter_content(chunk_size=8192):
  88. f.write(chunk)
  89. logger.info(f"成功下载图片: {output_path}")
  90. return output_path
  91. except Exception as e:
  92. logger.error(f"下载图片失败: {e}")
  93. raise Exception(f"下载图片失败: {str(e)}")
  94. def remove_background_rembg(input_path, output_path):
  95. """
  96. 使用rembg库进行抠图处理
  97. 参数:
  98. - input_path: 输入图片路径
  99. - output_path: 输出PNG图片路径(带透明背景)
  100. 返回:
  101. - 输出文件路径
  102. """
  103. if not REMBG_AVAILABLE:
  104. raise Exception("rembg库未安装,请运行: pip install rembg")
  105. try:
  106. # 读取原始图片
  107. with open(input_path, 'rb') as input_file:
  108. input_data = input_file.read()
  109. # 使用rembg进行抠图
  110. output_data = remove(input_data)
  111. # 保存为PNG格式(PNG支持透明通道)
  112. with open(output_path, 'wb') as output_file:
  113. output_file.write(output_data)
  114. logger.info(f"成功完成抠图处理: {output_path}")
  115. return output_path
  116. except Exception as e:
  117. logger.error(f"抠图处理失败: {e}")
  118. raise Exception(f"抠图处理失败: {str(e)}")
  119. def remove_background_simple(input_path, output_path):
  120. """
  121. 简单的背景移除方法(适用于简单背景)
  122. 如果rembg不可用,可以使用这个简单方法作为备选
  123. 参数:
  124. - input_path: 输入图片路径
  125. - output_path: 输出PNG图片路径
  126. 返回:
  127. - 输出文件路径
  128. """
  129. try:
  130. # 打开图片并转换为RGBA格式
  131. image = Image.open(input_path).convert("RGBA")
  132. datas = image.getdata()
  133. new_data = []
  134. # 简单阈值法:移除接近白色的背景
  135. threshold = 240 # 阈值,可调整
  136. for item in datas:
  137. # 如果RGB值都接近白色(大于阈值),则设为透明
  138. if item[0] > threshold and item[1] > threshold and item[2] > threshold:
  139. new_data.append((255, 255, 255, 0)) # 设置为透明
  140. else:
  141. new_data.append(item)
  142. image.putdata(new_data)
  143. image.save(output_path, "PNG")
  144. logger.info(f"使用简单方法完成抠图处理: {output_path}")
  145. return output_path
  146. except Exception as e:
  147. logger.error(f"简单抠图处理失败: {e}")
  148. raise Exception(f"抠图处理失败: {str(e)}")
  149. def trim_image(input_path, output_path):
  150. """
  151. 裁剪图片的空白边缘,只保留有图像的部分
  152. 参数:
  153. - input_path: 输入图片路径(应该是RGBA格式的PNG)
  154. - output_path: 输出裁剪后的图片路径
  155. 返回:
  156. - 输出文件路径
  157. """
  158. try:
  159. # 打开图片并确保是RGBA格式
  160. image = Image.open(input_path).convert("RGBA")
  161. # 获取图片的alpha通道(透明度通道)
  162. # 如果alpha值为0或接近0,则认为是透明/空白区域
  163. bbox = image.getbbox()
  164. if bbox is None:
  165. # 如果整个图片都是透明的,返回原图
  166. logger.warning("图片完全透明,无法裁剪")
  167. image.save(output_path, "PNG")
  168. return output_path
  169. # 裁剪图片到有效区域
  170. trimmed_image = image.crop(bbox)
  171. trimmed_image.save(output_path, "PNG")
  172. logger.info(f"成功裁剪图片: 原始尺寸 {image.size} -> 裁剪后尺寸 {trimmed_image.size}")
  173. return output_path
  174. except Exception as e:
  175. logger.error(f"裁剪图片失败: {e}")
  176. raise Exception(f"裁剪图片失败: {str(e)}")
  177. def process_image_url(image_url, use_rembg=True, trim_edges=False):
  178. """
  179. 处理图片:支持URL或base64编码 -> 抠图 -> (可选)裁剪空白边缘 -> 保存为PNG -> 上传到七牛云
  180. 参数:
  181. - image_url: 图片URL或base64编码的图片字符串
  182. - use_rembg: 是否使用rembg库(True)或简单方法(False)
  183. - trim_edges: 是否裁剪空白边缘(适用于公章等需要去除空白的图片)
  184. 返回:
  185. - 七牛云上的新图片URL
  186. """
  187. temp_input = None
  188. temp_output = None
  189. temp_trimmed = None
  190. try:
  191. # 创建临时目录
  192. temp_dir = "./temp"
  193. if not os.path.exists(temp_dir):
  194. os.makedirs(temp_dir)
  195. # 检测输入类型:URL还是base64
  196. is_base64 = is_base64_image(image_url)
  197. if is_base64:
  198. # 处理base64图片
  199. logger.info("检测到base64编码的图片")
  200. # base64图片默认保存为.png(因为通常base64传输PNG或JPEG)
  201. file_ext = '.png'
  202. temp_input = os.path.join(temp_dir, f"input_{uuid.uuid4()}{file_ext}")
  203. save_base64_image(image_url, temp_input)
  204. else:
  205. # 处理URL图片
  206. logger.info(f"检测到URL图片: {image_url[:100]}...")
  207. # 从URL中提取文件扩展名,如果没有则默认为.jpg
  208. url_path = image_url.split('?')[0] # 移除查询参数
  209. file_ext = os.path.splitext(url_path)[1] or '.jpg'
  210. # 确保扩展名是常见的图片格式
  211. if file_ext.lower() not in ['.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp']:
  212. file_ext = '.jpg'
  213. # 下载原始图片
  214. temp_input = os.path.join(temp_dir, f"input_{uuid.uuid4()}{file_ext}")
  215. download_image_from_url(image_url, temp_input)
  216. # 检查文件是否存在
  217. if not os.path.exists(temp_input):
  218. raise Exception("下载的图片文件不存在")
  219. # 抠图处理
  220. temp_output = os.path.join(temp_dir, f"output_{uuid.uuid4()}.png")
  221. if use_rembg and REMBG_AVAILABLE:
  222. remove_background_rembg(temp_input, temp_output)
  223. else:
  224. if use_rembg:
  225. logger.warning("rembg不可用,使用简单方法")
  226. remove_background_simple(temp_input, temp_output)
  227. # 检查输出文件是否存在
  228. if not os.path.exists(temp_output):
  229. raise Exception("抠图处理后的文件不存在")
  230. # 如果需要裁剪空白边缘(适用于公章)
  231. final_output = temp_output
  232. if trim_edges:
  233. temp_trimmed = os.path.join(temp_dir, f"trimmed_{uuid.uuid4()}.png")
  234. trim_image(temp_output, temp_trimmed)
  235. final_output = temp_trimmed
  236. # 上传到七牛云
  237. file_key = f"UpImage/{uuid.uuid4()}.png"
  238. new_image_url = Qiniu.upload_to_qiniu(final_output, file_key)
  239. logger.info(f"成功处理图片并上传: {new_image_url}")
  240. return new_image_url
  241. except Exception as e:
  242. logger.error(f"处理图片失败: {e}")
  243. raise
  244. finally:
  245. # 清理临时文件
  246. try:
  247. if temp_input and os.path.exists(temp_input):
  248. os.remove(temp_input)
  249. if temp_output and os.path.exists(temp_output):
  250. os.remove(temp_output)
  251. if temp_trimmed and os.path.exists(temp_trimmed):
  252. os.remove(temp_trimmed)
  253. except Exception as e:
  254. logger.warning(f"清理临时文件失败: {e}")
  255. def create_process_image_route(app):
  256. """
  257. 创建图片处理接口路由
  258. 参数:
  259. - app: Flask应用实例
  260. """
  261. @app.route('/process_image', methods=['POST'])
  262. def process_image():
  263. """
  264. 图片抠图处理接口
  265. 请求参数(JSON):
  266. {
  267. "image_url": "图片URL链接 或 base64编码的图片字符串",
  268. "use_rembg": true, // 可选,是否使用rembg库(默认true)
  269. "is_seal": false // 可选,是否是公章(默认false)。如果是公章,会自动裁剪空白边缘
  270. }
  271. 注意:
  272. - image_url 支持两种格式:
  273. 1. HTTP/HTTPS URL链接
  274. 2. base64编码的图片(支持 "data:image/png;base64,..." 格式或纯base64字符串)
  275. 返回:
  276. {
  277. "success": true,
  278. "image_url": "处理后的图片URL",
  279. "message": "处理成功"
  280. }
  281. """
  282. try:
  283. # 获取请求参数
  284. data = request.get_json()
  285. if not data:
  286. return jsonify({
  287. "success": False,
  288. "error": "请求参数为空"
  289. }), 400
  290. image_url = data.get('image_url')
  291. if not image_url:
  292. return jsonify({
  293. "success": False,
  294. "error": "缺少image_url参数"
  295. }), 400
  296. # 可选参数:是否使用rembg
  297. use_rembg = data.get('use_rembg', True)
  298. # 可选参数:是否是公章(公章需要裁剪空白边缘)
  299. is_seal = data.get('is_seal', False)
  300. # 处理图片
  301. new_image_url = process_image_url(image_url, use_rembg=use_rembg, trim_edges=is_seal)
  302. return jsonify({
  303. "success": True,
  304. "image_url": new_image_url,
  305. "message": "图片处理成功"
  306. })
  307. except Exception as e:
  308. logger.error(f"接口处理失败: {e}")
  309. return jsonify({
  310. "success": False,
  311. "error": str(e)
  312. }), 500