叙述 书接上回：代码生成网站专属二维码 有宝宝反应，在有遮挡的情况下（怎么会遮挡呢？这种情况怎么能出现？我实在没考虑这种情况，但是为了更好的用户体验，即使这是一张小小的二维码图片也要根据反馈使它更完美）二维码扫不出来，好了，已解决。全部的代码可以前往 我的colab 查看。 为了保证二维码在不完全显示的情况下能够被成功扫描，不说了，直接上代码 下面是针对这些调整的改进代码： 代码 import qrcode from PIL import Image, ImageEnhance, ImageSequence # 生成二维码并提高容错级别 data = "https://www.example.com" # 二维码内容 qr = qrcode.QRCode( version=2, # 适中的二维码大小 error_correction=qrcode.constants.ERROR_CORRECT_H, # 高容错率 box_size=10, # 每个二维码格子的像素大小 border=1, # 边框减少 ) qr.add_data(data) qr.make(fit=True) # 生成二维码图片，并转换为 RGBA 模式，支持透明背景 qr_img = qr.make_image(fill='black', back_color='white').convert('RGBA') # 将二维码的白色部分替换为透明 datas = qr_img.getdata() new_data = [] for item in datas: # 将白色 (255, 255, 255) 替换为透明 if item[:3] == (255, 255, 255): # RGB是白色时 new_data.append((255, 255, 255, 0)) # 透明 else: new_data.append(item) qr_img.putdata(new_data) # 打开动态GIF图片 gif_path = "/content/your_dynamic_background.gif" # 替换为你的GIF路径 gif = Image.open(gif_path) # 设置需要的背景图和二维码的尺寸 new_size = (200, 200) # 目标大小，可以根据需要调整 # 调整二维码的大小 qr_img = qr_img.resize(new_size, Image.LANCZOS) # 用于存储处理后的帧 frames = [] # 遍历GIF的每一帧 for frame in ImageSequence.Iterator(gif): # 将每一帧转换为 RGBA 模式 frame = frame.convert('RGBA') # 调节背景图片的透明度，保证背景不过度干扰二维码 alpha = 0.3 # 设置透明度，值越小二维码越清晰，背景越不显眼 enhancer = ImageEnhance.Brightness(frame) frame = enhancer.enhance(alpha) # 调整每一帧的大小与二维码一致 frame = frame.resize(new_size, Image.LANCZOS) # 创建一个半透明的白色蒙版，进一步减少背景干扰 mask = Image.new('RGBA', qr_img.size, (255, 255, 255, 180)) # 半透明白色 # 将蒙版叠加到帧上 frame.paste(mask, (0, 0), mask) # 将二维码叠加到帧上 frame.paste(qr_img, (0, 0), qr_img) # 添加处理后的帧到frames列表中 frames.append(frame) # 保存结果为新的GIF，保留原GIF的帧速率和循环设置 frames[0].save( "qrcode_with_optimized_dynamic_background.gif", save_all=True, append_images=frames[1:], # 添加其他帧 duration=gif.info['duration'], # 使用原始的帧持续时间 loop=gif.info.get('loop', 0) # 保留循环设置 ) 改进点 高容错率：通过设置 error_correction=qrcode.constants.ERROR_CORRECT_H，二维码有 30% 的损坏空间仍然可以被正确识别。这允许部分二维码被遮盖或干扰而不影响扫描效果。 背景透明度调节：通过 alpha = 0.3 减少背景对二维码的干扰，保持二维码在背景上更加清晰。可以根据需要调整这个值。 半透明蒙版：添加一个半透明白色蒙版（透明度值 180），进一步减少背景图对二维码扫描的干扰，使得二维码部分更加显眼。 二维码大小调整：二维码和背景都调整为同等大小（不要过小），确保二维码有足够的像素以提高可读性。
简述 万圣节快到了，需要一些万圣节主题的图片制作成海报，到网站一张一张下载太过麻烦，就想着实现一个简单的Python爬虫工具来获取万圣节相关的图片，由于一些网站可能会通过robots.txt或其他手段禁止爬虫行为，你应当首先确认该网站是否允许被爬取，爬取内容时应当遵守网站的robots.txt规则以及相关的使用条款，避免非法爬取。我今天要爬取的网站是https://www.istockphoto.com/, 该站是允许爬取的。好了，开始了，可以使用requests和BeautifulSoup来爬取网页内容。另外，istockphoto网站通常会使用JavaScript加载图片，这意味着仅使用requests和BeautifulSoup可能无法抓取动态加载的内容。在这种情况下，可以考虑使用Selenium或Playwright来处理JavaScript渲染。 我今天实现的是基于Selenium的爬虫，它能够访问https://www.istockphoto.com/并查找与“万圣节”相关的图片。 准备：安装Selenium库和ChromeDriver pip install selenium 安装Selenium库, 用于模拟浏览器行为，特别是加载JavaScript内容。 安装一个WebDriver, 我使用的是ChromeDriver（注意控制Chrome浏览器，你需要下载对应版本的ChromeDriver并设置其路径）ChromeDriver的安装不再赘述，下面简短的说一下windows系统的安装与设置吧： 步骤 1: 检查你的Chrome浏览器版本 ChromeDriver的版本必须与已安装的Chrome浏览器版本匹配。 打开Chrome浏览器。 在地址栏中输入 chrome://settings/help，然后按下回车。 你会看到当前的Chrome版本号。 步骤 2: 下载与Chrome版本匹配的ChromeDriver 访问ChromeDriver官网。 查找与你的Chrome浏览器版本匹配的ChromeDriver版本（对于版本 115 及更高版本，）您可以在 Chrome for Testing (CfT) 可用性信息中心查看各个发布渠道（稳定版、Beta 版、开发者版、Canary 版）的最新 ChromeDriver 版本。。 下载适合你操作系统的文件（Windows, Mac, 或 Linux，我用的是win的）。 步骤 3: 安装ChromeDriver Windows用户 下载的chromedriver_win32.zip文件解压缩后，你会得到chromedriver.exe。 将这个文件放置在一个路径中，例如：C:\chromedriver\chromedriver.exe。 将这个路径加入系统的环境变量： 右键点击“此电脑”，选择“属性”。 选择“高级系统设置” → “环境变量”。 在“系统变量”区域中，找到“Path”，选择它并点击“编辑”。 点击“新建”，输入C:\chromedriver\，然后点击“确定”。 步骤 4: 测试ChromeDriver 打开终端或命令提示符，输入 chromedriver --version 如果成功安装，你应该会看到类似于 ChromeDriver 117.0.5938.62 这样的版本号信息。 步骤 5: 使用Selenium调用ChromeDriver 在Python代码中，确保你的Selenium脚本能够找到ChromeDriver。例如，如果没有将ChromeDriver添加到系统路径，可以在代码中直接提供其路径。 from selenium import webdriver from selenium.webdriver.chrome.service import Service # 指定chromedriver的路径 service = Service(executable_path='你的chromedriver所在路径') # 启动浏览器 driver = webdriver.Chrome(service=service) 代码实现 直接上代码（注释写的很清楚，我相信你们可以看明白）代码实现主要的点： 中途提示：防止无休止，每下载完20张图片后（可根据实际情况自行修改这个数值），程序会暂停，等待用户的输入。 并行下载：提高下载速度，使用多线程下载来并行处理多个图片的下载任务。每次批次处理后重新创建线程池：通过在 while 循环中每次处理完一批图片后，使用 with ThreadPoolExecutor 来创建一个新的线程池。线程池在每一批次结束后会自动关闭，下一次处理时重新创建一个新的线程池。 使用断点续传：如果网络中断或者长时间运行，可以记录已下载的图片，避免重复下载。 # Author: Bruce Kevin Chen # Create code: 2024/10/1 21:22 import os import requests from selenium import webdriver from selenium.webdriver.common.by import By from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.options import Options from concurrent.futures import ThreadPoolExecutor import time # 配置ChromeDriver chrome_options = Options() chrome_options.add_argument("--headless") # 在后台运行 chrome_options.add_argument("--no-sandbox") chrome_options.add_argument("--disable-dev-shm-usage") # 替换为你自己的ChromeDriver路径 service = Service(executable_path='你的chromedriver.exe路径') # 创建halloween目录，如果不存在 save_dir = 'halloween' if not os.path.exists(save_dir): os.makedirs(save_dir) # 下载图片的函数 def download_image(url, idx): try: img_data = requests.get(url).content file_path = os.path.join(save_dir, f'halloween_image_{idx + 1}.jpg') with open(file_path, 'wb') as handler: handler.write(img_data) print(f"Image {idx + 1} downloaded: {url}") except Exception as e: print(f"Failed to download image {idx + 1}: {e}") # 启动浏览器 driver = webdriver.Chrome(service=service, options=chrome_options) # 打开目标网站 driver.get("https://www.istockphoto.com/photos/halloween") # 确保页面加载完成 time.sleep(3) # 找到图片元素 image_elements = driver.find_elements(By.CSS_SELECTOR, 'img') # 提取图片的src属性 image_urls = [] for idx, img in enumerate(image_elements): src = img.get_attribute('src') if src and 'https://media.istockphoto.com' in src: image_urls.append((src, idx)) # 关闭浏览器 driver.quit() # 使用线程池并行下载 def download_in_batches(image_urls, batch_size=20): total_images = len(image_urls) current_batch = 0 while current_batch * batch_size < total_images: start = current_batch * batch_size end = min((current_batch + 1) * batch_size, total_images) batch = image_urls[start:end] # 每批次创建一个新的线程池 # 在每一批次的下载中创建一个新的ThreadPoolExecutor，而不是在整个过程中只使用一个线程池。 # 这样每次询问用户是否继续下载时，都会创建一个新的线程池来处理下载任务，防止由于 executor.shutdown(wait=True) 方法 调用后， # 线程池被关闭，导致无法提交新的任务。在关闭线程池之后，不能再使用同一个线程池提交新的任务。 with ThreadPoolExecutor(max_workers=5) as executor: # 下载当前批次的图片 for url, idx in batch: executor.submit(download_image, url, idx) current_batch += 1 # 如果下载了20张，询问用户是否继续 if current_batch * batch_size < total_images: user_input = input(f"{end} images downloaded. Do you want to download more? (y/n): ") if user_input.lower() != 'y': print("Download stopped by user.") break # 每批次下载20张图片，并询问用户是否继续 download_in_batches(image_urls, batch_size=20)
最终的效果图如下： 想想怎么推广网站或者分享网站的文章呢？想想前些日子帮原同事简单生成了一批保存人员代码的二维码，想着我可以为自己的网站生成二维码用以推广啊，和先前一样使用 qrcode 库来生成二维码图片。 下面是简单生成的示例代码，使用 Python 生成二维码图片并保存为 PNG 文件。 如果尚未安装 qrcode 库，可以通过以下命令安装它： pip install qrcode[pil] 1. 简单示例 import qrcode # 定义二维码内容 data = "https://www.example.com" # 创建QRCode对象 qr = qrcode.QRCode( version=1, # 控制二维码大小，1是21x21的大小 error_correction=qrcode.constants.ERROR_CORRECT_L, # 容错率 box_size=10, # 每个二维码格子的像素大小 border=4, # 边框宽度 ) # 将数据添加到二维码中 qr.add_data(data) qr.make(fit=True) # 创建二维码图片 img = qr.make_image(fill='black', back_color='white') # 保存二维码图片 img.save("qrcode.png") 你可以根据需要修改二维码的内容、样式或保存路径。 2. 将网站logo加入到二维码正中央 使用PIL.Image 模块中的 Image.LANCZOS 作为图像缩放的过滤器 import qrcode from PIL import Image # 生成二维码 data = "https://www.example.com" # 二维码内容 qr = qrcode.QRCode( version=1, # 控制二维码大小 error_correction=qrcode.constants.ERROR_CORRECT_H, # 提高容错率 box_size=10, # 每个二维码格子的像素大小 border=4, # 边框宽度 ) qr.add_data(data) qr.make(fit=True) # 生成二维码图片 qr_img = qr.make_image(fill='black', back_color='white').convert('RGB') # 打开并调整网站logo大小 logo_path = "/content/favicon.ico" logo = Image.open(logo_path) # 根据二维码的大小调整logo的大小 qr_width, qr_height = qr_img.size logo_size = int(qr_width * 0.2) # logo的大小为二维码的20% logo = logo.resize((logo_size, logo_size), Image.LANCZOS) # 使用 LANCZOS 替代 ANTIALIAS # 计算logo放置在二维码中央的位置 logo_pos = ((qr_width - logo_size) // 2, (qr_height - logo_size) // 2) # 将logo粘贴到二维码上 qr_img.paste(logo, logo_pos, mask=logo) # 保存二维码图片 qr_img.save("qrcode_with_logo.png") # 显示生成的二维码 qr_img.show() 3. 以Logo为背景图生成二维码 logo图颜色太过鲜艳显眼，可以同时降低它的透明度并添加一个半透明的蒙版来增强二维码的可读性。下面是结合了调节背景图透明度和添加半透明蒙版的代码： import qrcode from PIL import Image, ImageEnhance # 生成二维码 data = "https://www.example.com" # 二维码内容 qr = qrcode.QRCode( version=1, # 控制二维码大小 error_correction=qrcode.constants.ERROR_CORRECT_H, # 提高容错率 box_size=20, # 每个二维码格子的像素大小，以适应背景图 border=0, # 去除二维码边框 ) qr.add_data(data) qr.make(fit=True) # 生成二维码图片，并转换为 RGBA 模式，支持透明背景 qr_img = qr.make_image(fill='black', back_color='white').convert('RGBA') # 打开logo图片，并调整大小为200x200 logo_path = "/content/favicon.ico" logo = Image.open(logo_path).convert("RGBA") logo = logo.resize((200, 200), Image.LANCZOS) # 调节背景图片的透明度 alpha = 0.5 # 设置透明度，0表示完全透明，1表示不透明 enhancer = ImageEnhance.Brightness(logo) logo = enhancer.enhance(alpha) # 创建一个半透明的白色蒙版 mask = Image.new('RGBA', qr_img.size, (255, 255, 255, 128)) # 半透明白色 # 调整二维码图片大小为200x200 qr_img = qr_img.resize((200, 200), Image.LANCZOS) # 将二维码的白色部分替换为透明 datas = qr_img.getdata() new_data = [] for item in datas: # 将白色 (255, 255, 255) 替换为透明 if item[:3] == (255, 255, 255): # RGB是白色时 new_data.append((255, 255, 255, 0)) # 透明 else: new_data.append(item) qr_img.putdata(new_data) # 在背景图上先叠加半透明蒙版 logo.paste(mask, (0, 0), mask) # 然后将二维码叠加到背景图上 logo.paste(qr_img, (0, 0), qr_img) # 保存并显示结果图片 logo.save("qrcode_with_combined_effects.png") logo.show() 关键步骤 调节背景图的透明度：通过 ImageEnhance.Brightness 降低背景图的亮度，alpha 控制透明度（在这里设置为 0.5，可以根据需求调整）。 添加半透明蒙版：创建一个白色的半透明蒙版，将其叠加在背景图上，使背景颜色更加柔和，增强二维码的可读性。 调整二维码透明度：将二维码的白色部分替换为透明，以使背景图透过透明区域显示。 将二维码叠加到背景图上：在调节过透明度和添加蒙版的背景图上叠加二维码，使两者更加协调。 4. 进一步将背景图换成动态图（GIF类型） 将动态 GIF 图像作为二维码的背景并保持其动态效果，需要逐帧处理 GIF 图像。对于每一帧，使用以上类似的操作来处理背景透明度、添加蒙版，并将二维码叠加在每一帧上，最后将这些帧合并为新的 GIF 文件。可调节大小，在处理 GIF 图像的过程中调整背景和二维码的大小。可以在每一帧上调整 GIF 背景图像和二维码的尺寸，使它们匹配，按需要进行缩放。 可以通过调整每一帧和二维码的大小来实现这个功能。以下是它的代码： import qrcode from PIL import Image, ImageEnhance, ImageSequence # 生成二维码 data = "https://www.example.com" # 二维码内容 qr = qrcode.QRCode( version=1, # 控制二维码大小 error_correction=qrcode.constants.ERROR_CORRECT_H, # 提高容错率 box_size=10, # 每个二维码格子的像素大小 border=0, # 去除二维码边框 ) qr.add_data(data) qr.make(fit=True) # 生成二维码图片，并转换为 RGBA 模式，支持透明背景 qr_img = qr.make_image(fill='black', back_color='white').convert('RGBA') # 将二维码的白色部分替换为透明 datas = qr_img.getdata() new_data = [] for item in datas: # 将白色 (255, 255, 255) 替换为透明 if item[:3] == (255, 255, 255): # RGB是白色时 new_data.append((255, 255, 255, 0)) # 透明 else: new_data.append(item) qr_img.putdata(new_data) # 打开动态GIF图片 gif_path = "/content/your_dynamic_background.gif" # 替换为你的GIF路径 gif = Image.open(gif_path) # 设置需要的背景图和二维码的尺寸 new_size = (200, 200) # 目标大小，可以根据需要调整 # 调整二维码的大小 qr_img = qr_img.resize(new_size, Image.LANCZOS) # 用于存储处理后的帧 frames = [] # 遍历GIF的每一帧 for frame in ImageSequence.Iterator(gif): # 将每一帧转换为 RGBA 模式 frame = frame.convert('RGBA') # 调节背景图片的透明度 alpha = 0.5 # 设置透明度，0表示完全透明，1表示不透明 enhancer = ImageEnhance.Brightness(frame) frame = enhancer.enhance(alpha) # 调整每一帧的大小与二维码一致 frame = frame.resize(new_size, Image.LANCZOS) # 创建一个半透明的白色蒙版 mask = Image.new('RGBA', qr_img.size, (255, 255, 255, 128)) # 半透明白色 # 将蒙版叠加到帧上 frame.paste(mask, (0, 0), mask) # 将二维码叠加到帧上 frame.paste(qr_img, (0, 0), qr_img) # 添加处理后的帧到frames列表中 frames.append(frame) # 保存结果为新的GIF，保留原GIF的帧速率和循环设置 frames[0].save( "qrcode_with_dynamic_background_resized.gif", save_all=True, append_images=frames[1:], # 添加其他帧 duration=gif.info['duration'], # 使用原始的帧持续时间 loop=gif.info.get('loop', 0) # 保留循环设置 ) 调节大小的步骤 二维码大小调整：使用 qr_img.resize(new_size, Image.LANCZOS) 通过 new_size 参数设置二维码的目标大小。例如，new_size = (200, 200) 调整为 200x200 像素。 背景图帧大小调整：在每一帧上通过 frame.resize(new_size, Image.LANCZOS) 同样将 GIF 背景调整到目标大小 new_size。 蒙版大小调整：蒙版的大小与二维码和背景一致，通过 mask = Image.new('RGBA', qr_img.size, ...) 创建与二维码大小相同的蒙版。 结果 生成的动态 GIF 将保持背景的动态效果，并且二维码和背景图都将按照指定的尺寸进行缩放。如果希望二维码比背景稍小或者更大，可以分别调整二维码和背景的 resize 参数。
小鱼特效与回到顶部功能 效果可参考本网站 你可以将这段代码分开放在你的css、js和html（或者php）文件中，最直接方便的方式是一股脑放在你底部页面的html（或者php）文件中, fish.js文件参考网页底部小鱼游动特效 <?php if (!defined("__TYPECHO_ROOT_DIR__")) { exit(); } ?> 备案号 <?php $this->footer(); ?> 顶部 排版后的代码 <?php if (!defined("__TYPECHO_ROOT_DIR__")) { exit(); } ?> 备案号 Top <?php $this->footer(); ?> 继续 <?php if (!defined("__TYPECHO_ROOT_DIR__")) { exit(); } ?> xxx Top <?php $this->footer(); ?>
[研究学习日记] 当 Segment Anything 大模型遇上医学 X 光：从零样本到动态提示工程的探索
在先前的硕士课程 Advanced Deep Learning 中，我们的课程作业带来了一个极其硬核且紧跟前沿的课题—探索 Meta AI 提出的颠覆性视觉基础大模型：Segment Anything Model (SAM) 。 SAM 在计算机视觉领域的地位，就如同 GPT 在自然语言处理领域的地位一样。它通过海量数据（1100 万张图片，超 11 亿个掩码的 SA-1B 数据集）的洗礼，实现了强大的 Zero-shot（零样本）泛化能力，并引入了优雅的 Promptable Segmentation（可提示分割） 范式。 然而，SAM 原论文主要在自然场景数据集上进行训练和评估。作为一名严谨的学习者，我产生了一个巨大的疑问：当号称能“分割一切”的 SAM，面对它极少见过的、对比度低且缺乏自然特征的医学 X 光（X-ray）图像时，它还能保持神奇的魔力吗？ 这篇博客既是我的课程学习日记，也是一次技术复盘。我将带大家从阅读 SAM 论文开始，体验 Web 端的交互分割，到使用 Python 探索 Prompt Engineering，最后自己动手搭建 YOLOv8 动态提示（Dynamic Prompting）+ SAM 的全自动流水线，并与传统的 U-Net 进行一场“终极对决”。 代码链接：Segmenting Lung X-ray Images with the Segment Anything Model 1. 理论碰撞：SAM 能胜任医疗 X 光分割吗？ 在动手敲代码之前，作业的第一步要求我们基于原论文《Segment Anything》评估 SAM 在医疗 X-ray 肺部提取任务上的预期表现。我的理论分析如下： 挑战在于领域差异（Domain Gap）： 医学 X 光图像与自然图像存在巨大鸿沟。X 光片是灰度图，对比度低，且包含许多需要专业医学先验知识才能辨别的细微解剖结构。SAM 作为通用模型，没有经过大规模医疗数据的专门微调，在处理复杂的器官边界时可能会“水土不服”。 潜力在于极强的泛化（Generalizability）： SAM 的核心卖点是其强大的零样本适应能力。得益于在海量数据上学习到的底层边缘、形状和连通区域特征，只要我们能给出精准的提示（Prompt，如明确的点或框），它极大可能具备克服领域差异的能力。 初步结论： 我预期 SAM 能给出一个相当不错的 Baseline，但极度依赖于高质量的提示（Prompt）。 2. 浅尝辄止：Web Demo 上的交互式分割初体验 俗话说实践出真知，我首先在 SAM 官方 Demo 网站 上上传了几张验证集的肺部 X 光片，分别体验了三种不同的 Prompt 方式： 点提示（Point Prompting）： 提供了极高的控制力。可以通过不断添加绿点（前景）和红点（背景）来引导模型，精细修正边界。缺点是费时费力，如果初始点选得偏，生成的初始掩码会很不理想。 边界框提示（Bounding Box Prompting）： 体验极佳！针对肺部这种大型解剖结构，画一个大致的框，SAM 瞬间就能利用全局上下文（Context）捕捉到器官边界，初始分割效果往往比单点好得多且速度更快。 全自动模式（Automatic Mode）： 全自动生成，但在 X 光片上简直是“灾难现场”。由于缺乏明确的医学语义目标，它把肺部、心脏、肋骨、背景仪器切成了无数个毫无意义的细碎小块，产生严重的过分割。 图 1：SAM的三种不同提示（Prompt）方式分割效果对比 这也印证了我的猜想：SAM 在医学图像上是有效的，但必须有人工或算法的精确引导。 3. 代码实战：Python 里的静态 Prompt Engineering 体验完 Demo，真正的硬核挑战开始了。我们在 Jupyter Notebook 中使用 Python API 来调用 SAM 处理验证集。 策略 A：启发式点提示 (Point Prompt) 我为单侧肺部精心设计了固定的坐标提示点组合：4个前景点（如 [75, 100], [80, 75], [180, 75], [185, 100]）加上 1个背景点（位于纵隔区域 [135, 150] 以防溢出）。 优化 Trick：Mask 级联预测 为了获得最佳结果，我利用了 SAM 输出多候选掩码的特性：首先让模型初步预测，提取置信度（Score）最高的那个 Mask，然后 将其作为下一次预测的输入先验（Input Mask） 进行细化打磨。 验证集结果：Mean F1-Score = 0.9084，Std = 0.0247 对于一个未经任何医疗数据微调的 Zero-shot 模型，仅靠 5 个固定坐标点就能在肺部提取上达到 90% 以上的 F1 分数，不仅精度高而且表现极其稳定！ 策略 B：真实边界框提示 (Ground Truth BBox Prompt) 基于 Web 端的经验，我写了一个脚本，利用真实分割掩码（Ground Truth）通过 OpenCV (cv2.boundingRect) 提取出极其准确的边界框，并将其作为 Prompt 喂给 SAM。最后采用 np.logical_or 合并左右肺的输出。 验证集结果：Mean F1-Score = 0.9177，Std = 0.0248 图 2：基于真实边界框（GT BBox）作为提示的 SAM 肺部提取效果 代码实验感悟： 边界框（BBox）为 SAM 提供了比散点更明确的全局空间约束，有效排除了医学图像中的噪声干扰，因此 F1 分数获得了进一步提升。 4. 终极形态：动态提示流水线 (YOLOv8 + SAM) 静态点和真实框提示虽然得分高，但在实际临床的自动诊断中，医生不可能一张张图去打点，我们也没有 Ground Truth 可以提取框。有没有办法实现完全的端到端自动化？ 这就是本次作业的终极进阶：动态提示（Dynamic Prompting）。既然框提示效果最好，那我们为什么不训练一个轻量级的目标检测网络，让它自动找出肺部的 Bounding Box，再喂给 SAM 呢？ 我的全自动流水线设计如下： 训练检测器 (YOLOv8)： 使用上一步从 GT 中提取的边界框作为标签，我专门训练了一个 YOLOv8 目标检测模型（跑了 250 个 Epochs）。它的任务很简单：学会在 X 光片中定位双肺。事实证明它做得极好，在测试集上的 Mean IoU 达到了 0.9364。 强强联手 (YOLOv8 -> SAM)： 给定一张全新的 X 光图 $\rightarrow$ YOLOv8 预测边界框 $\rightarrow$ 将其作为动态 Prompt 传给 SAM $\rightarrow$ SAM 零样本输出高精度精细掩码。 图 3：YOLOv8 目标检测 + SAM 动态提示的全自动分割预测 终极对决 在这门课的先前任务中，我们曾完全从零开始手工训练了一个经典的 U-Net 来做肺部自动分割。现在，让我们看看“基础大模型”带来的降维打击（在测试集上的表现）： 分割方法 / 模型 提示机制 Mean F1-Score Standard Deviation Vanilla U-Net 全监督端到端训练 0.6073 0.0901 SAM (Zero-shot) 静态点提示 (Points) 0.9084 0.0247 SAM (Zero-shot) 真实框提示 (GT BBox) 0.9177 0.0248 YOLOv8 + SAM 动态框提示 (Dynamic) 0.9233 0.0280 核心结论剖析： 对传统小模型的绝对碾压： 相比于辛辛苦苦从头训练的 U-Net（F1 仅有 0.6073，且极不稳定 Std=0.0901），基于大模型的 YOLOv8+SAM 管道在精度（0.9233）和鲁棒性（Std=0.0280）上实现了彻底的降维打击。 分工明确的优雅架构： YOLOv8 + SAM 取得了全场最高分！YOLOv8 完美弥补了 SAM 缺乏特定医学先验的短板，充当了“放射科专家”的眼睛（高精度定位）；而 SAM 充分发挥了它强大的通用基础能力，担当了“像素级描边大师”。两者结合，在不对 SAM 进行任何权重微调的情况下，实现了极高质量的自动化医学分割。 5. 总结与反思 通过这次 Advanced Deep Learning 课程的深度探索，我对计算机视觉领域正在发生的 范式转变（Paradigm Shift） 有了切肤之感。 过去，面对一个新的医学图像分割任务，我们总是苦于收集大量精细的像素级标注数据，耗时耗力去从零训练或微调一个 U-Net。 而在 Foundation Models（基础大模型） 时代，视觉任务也正在不可逆转地走向 Prompt Engineering（提示工程）。 SAM 展现出了一个强大底层视觉引擎的魅力。“轻量级目标检测模型 (Task-specific) + 通用分割大模型 (General)” 的解耦架构展现出了巨大的落地潜力。我们只需要极低的成本训练一个 YOLO 来充当“提示器”，繁重的抠图工作直接交给 SAM 即可。 虽然在极个别肺尖或肋膈角的模糊细节上，SAM 仍有一丝偏移，但瑕不掩瑜。Segment Anything 名副其实，它为深度学习在医疗垂直影像领域的落地，开启了一个激动人心的新范式！ 感谢阅读！本文为哥本哈根大学 Advanced Deep Learning 课程作业复盘与个人探索记录。如果你也对视觉大模型在医疗领域的应用感兴趣，欢迎联系我一起探讨交流！
开场 书接上回，为了实现更复杂的需求，如多层嵌套映射，我们可以通过在 MyBatis 中嵌套多个 resultMap 来实现。例如，假设我们现在有三个表，它们之间存在多层嵌套关系：Category（类别）、Product（产品）和 Review（评论）。一个 Category 有多个 Product，每个 Product 又有多个 Review，这样就形成了多层嵌套的结构。 示例场景 假设有以下三个表： category 表： CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) ); product 表： CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), price DECIMAL(10, 2), category_id INT, FOREIGN KEY (category_id) REFERENCES category(id) ); review 表： CREATE TABLE review ( id INT PRIMARY KEY AUTO_INCREMENT, content VARCHAR(255), rating INT, product_id INT, FOREIGN KEY (product_id) REFERENCES product(id) ); 1. 实体类设计 Category 实体类 package com.example.entity; import lombok.Data; import java.util.List; @Data public class Category { private Integer id; private String name; private List<Product> products; // 一对多关系，产品列表 } Product 实体类 package com.example.entity; import lombok.Data; import java.util.List; @Data public class Product { private Integer id; private String name; private Double price; private Integer categoryId; private List<Review> reviews; // 一对多关系，评论列表 } Review 实体类 package com.example.entity; import lombok.Data; @Data public class Review { private Integer id; private String content; private Integer rating; private Integer productId; } 2. Mapper 接口设计 CategoryMapper.java package com.example.mapper; import com.example.entity.Category; import org.apache.ibatis.annotations.Select; import java.util.List; public interface CategoryMapper { // 查询所有类别，包括其产品和产品的评论信息（多层嵌套） @Select("SELECT * FROM category") List<Category> findAllCategoriesWithProductsAndReviews(); } 3. MyBatis 配置文件 我们需要在 CategoryMapper.xml 中定义嵌套的 resultMap 来处理多层嵌套的关系。 CategoryMapper.xml <?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.example.mapper.CategoryMapper"> <resultMap id="CategoryProductReviewMap" type="com.example.entity.Category"> <id column="category_id" property="id"/> <result column="category_name" property="name"/> <collection property="products" ofType="com.example.entity.Product"> <id column="product_id" property="id"/> <result column="product_name" property="name"/> <result column="price" property="price"/> <result column="category_id" property="categoryId"/> <collection property="reviews" ofType="com.example.entity.Review"> <id column="review_id" property="id"/> <result column="content" property="content"/> <result column="rating" property="rating"/> <result column="product_id" property="productId"/> </collection> </collection> </resultMap> SELECT c.id AS category_id, c.name AS category_name, p.id AS product_id, p.name AS product_name, p.price AS product_price, p.category_id AS product_category_id, r.id AS review_id, r.content AS review_content, r.rating AS review_rating, r.product_id AS review_product_id FROM category c LEFT JOIN product p ON c.id = p.category_id LEFT JOIN review r ON p.id = r.product_id; </mapper> 4. Service 层与 Controller 实现 CategoryService.java package com.example.service; import com.example.entity.Category; import com.example.mapper.CategoryMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class CategoryService { @Autowired private CategoryMapper categoryMapper; public List<Category> findAllCategoriesWithProductsAndReviews() { return categoryMapper.findAllCategoriesWithProductsAndReviews(); } } CategoryController.java package com.example.controller; import com.example.entity.Category; import com.example.service.CategoryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class CategoryController { @Autowired private CategoryService categoryService; @GetMapping("/categories") public List<Category> getCategoriesWithProductsAndReviews() { return categoryService.findAllCategoriesWithProductsAndReviews(); } } 5. 测试 启动应用后，访问 http://localhost:8080/categories，你将会得到包含 Category、Product 和 Review 的完整多层嵌套的 JSON 结果。例如，返回的数据格式如下： [ { "id": 1, "name": "Electronics", "products": [ { "id": 1, "name": "Laptop", "price": 1000.00, "categoryId": 1, "reviews": [ { "id": 1, "content": "Great laptop!", "rating": 5, "productId": 1 }, { "id": 2, "content": "Not bad.", "rating": 4, "productId": 1 } ] } ] } ] 总结 通过这个复杂的多层嵌套映射示例，可以看到如何在 MyBatis 中实现一个 Category -> Product -> Review 的多对多层级关系。我们使用了 resultMap 和 collection 来处理每个层级的映射，确保每个实体类的关系能够被准确解析。通过这种方式，MyBatis 可以灵活地处理多层嵌套的查询和数据映射。
在 MyBatis 中实现一对多映射时，可以通过 resultMap 配置来实现。假设我们有两个表：Category 和 Product，一个 Category 可以有多个 Product，即这是一个一对多的关系。下面是一个具体的示例，展示如何使用 MyBatis 实现一对多映射。 示例场景 假设有以下两个表： category 表： CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255) ); product 表： CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(255), price DECIMAL(10, 2), category_id INT, FOREIGN KEY (category_id) REFERENCES category(id) ); 1. 创建实体类 Category 实体类 package com.example.entity; import lombok.Data; import java.util.List; @Data public class Category { private Integer id; private String name; private List<Product> products; // 一对多关系，产品列表 } Product 实体类 package com.example.entity; import lombok.Data; @Data public class Product { private Integer id; private String name; private Double price; private Integer categoryId; } 2. 创建 Mapper 接口 CategoryMapper.java package com.example.mapper; import com.example.entity.Category; import org.apache.ibatis.annotations.Select; import java.util.List; public interface CategoryMapper { // 通过 category id 查询所有产品的类别信息（包括产品信息） @Select("SELECT * FROM category") List<Category> findAllCategoriesWithProducts(); } 3. 配置一对多的 resultMap 在 CategoryMapper.xml 中定义 resultMap 来实现一对多映射。 CategoryMapper.xml <?xml version="1.0" encoding="UTF-8" ?> <mapper namespace="com.example.mapper.CategoryMapper"> <resultMap id="CategoryProductMap" type="com.example.entity.Category"> <id column="id" property="id" /> <result column="name" property="name" /> <collection property="products" ofType="com.example.entity.Product"> <id column="id" property="id" /> <result column="name" property="name" /> <result column="price" property="price" /> <result column="category_id" property="categoryId" /> </collection> </resultMap> SELECT c.id AS category_id, c.name AS category_name, p.id AS product_id, p.name AS product_name, p.price AS product_price, p.category_id AS product_category_id FROM category c LEFT JOIN product p ON c.id = p.category_id; </mapper> 4. Service 层与 Controller 实现 CategoryService.java package com.example.service; import com.example.entity.Category; import com.example.mapper.CategoryMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class CategoryService { @Autowired private CategoryMapper categoryMapper; public List<Category> findAllCategoriesWithProducts() { return categoryMapper.findAllCategoriesWithProducts(); } } CategoryController.java package com.example.controller; import com.example.entity.Category; import com.example.service.CategoryService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class CategoryController { @Autowired private CategoryService categoryService; @GetMapping("/categories") public List<Category> getCategoriesWithProducts() { return categoryService.findAllCategoriesWithProducts(); } } 5. 测试 启动应用后，访问 http://localhost:8080/categories，可以查看到 Category 及其 Product 列表。 总结 在这个实现中，我们通过以下几个步骤实现了 MyBatis 的一对多映射： 在数据库中建立一对多的外键关系。 在 MyBatis 中通过 resultMap 定义一对多的映射。 在 Service 层与 Controller 层调用一对多查询并返回结果。 这样我们就完成了一个简单的 MyBatis 一对多映射示例。如果有更多复杂的需求，也可以通过自定义查询和复杂的 resultMap 实现多层嵌套映射。
一、机器人设置 设置流程：如何设置群机器人 -帮助中心-企业微信 (qq.com) 记得将Webhook复制保存下来 二、机器人定时推送功能代码实现 推送消息模块代码 clock.py #!/usr/bin/python3 # -*- coding: utf-8 -*- import requests import json import pandas as pd import os # 企业微信机器人 Webhook webhook_url = 'https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=你的机器人KEY' # 发送文本消息 def send_text_message(content): headers = {'Content-Type': 'application/json'} data = { "msgtype": "text", "text": { "content": content } } response = requests.post(webhook_url, headers=headers, data=json.dumps(data)) return response.json() # 发送文件 def send_file_message(file_path): # 上传文件接口，需要先把文件上传，获取media_id upload_url = f'https://qyapi.weixin.qq.com/cgi-bin/webhook/upload_media?key=你的机器人KEY&type=file' with open(file_path, 'rb') as f: files = {'file': f} response = requests.post(upload_url, files=files) result = response.json() if result.get('media_id'): # 使用media_id发送文件消息 media_id = result['media_id'] data = { "msgtype": "file", "file": { "media_id": media_id } } headers = {'Content-Type': 'application/json'} send_response = requests.post(webhook_url, headers=headers, data=json.dumps(data)) return send_response.json() else: print("文件上传失败:", result) return result # 读取并处理 Excel 文件（假设文件名为 '白云指标.xlsx'） def process_excel_file(file_path): df = pd.read_excel(file_path) # 你可以在这里进行数据处理，例如读取特定数据 summary = df.describe() # 简单获取文件的统计信息 return summary def main(): file_path = '白云指标.xlsx' if os.path.exists(file_path): # 处理 Excel 文件 summary = process_excel_file(file_path) # 发送 Excel 文件 send_file_message(file_path) # 发送文本消息，附上文件的摘要信息 send_text_message(f"已发送文件: {file_path}\n文件摘要:\n{summary}") else: send_text_message(f"文件 {file_path} 不存在") if __name__ == "__main__": main() #!/usr/bin/python3 --python包所在路径，这里使用应用镜像是CentOs8.x，环境自带python3 --如果是python以下版本，请改成以下 #!/usr/bin/python 定时设置代码 使用crontab表达式实现 * * * * * python3 /文件路径 #* * * * * crontab表达式 #如果你的环境是python3以下版本，请把python3改成python #/文件路径 需要执行的py文件路径 #示例： 0 16 * * * python3 /root/clock.py #上述代码表示在每天下午四点整运行执行/root目录下的clock.py文件以此实现定时推送功能 crontab表达式说明 * * * * * - - - - - | | | | | | | | | +----- 星期中星期几 (0 - 6) (星期天为0) | | | +---------- 月份 (1 - 12) | | +--------------- 一个月中的第几天 (1 - 31) | +-------------------- 小时 (0 - 23) +------------------------- 分钟 (0 - 59) crontab验证工具 在线常用crontab表达式大全验证解析 - ToolTT 在线工具箱 命令行 vi clock.py 进入文件编辑界面，点击键盘i键进行文件内容写入，写入完毕后点击esc键退出，然后键盘输入 :wq保存退出 ./clock.py #运行测试看是否能运行clock.py 然后执行 crontab -e 进入crontab定时编辑界面，写入表达式 例如 0 16 * * * python3 /root/clock.py 然后按esc键退出编辑， 键盘输入 :wq 保存退出 你可将此代码文件和命令行在本地或者云服务器上执行。
在此例中，我们将采用 MyBatis-Plus进行快速开发，利用其内置的分页功能来实现前后端联动的分页展示。该实现包括项目初始化、后端代码编写以及前端页面的交互。 1 初始化此项目 新建项目勾选你项目所需的依赖库 在pom.xml中注入依赖：构造器所需依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.3.2</version> </depdency> <dependency> <groupId>org.apache.velocity</groupId> <artifactId>velocity</artifactId> <version>1.7</version> </dependency> MyBatis-Plus可以自动生成基础的 CRUD 代码，并提供了内置的分页功能，简化了分页查询的实现。 2 后端代码模块 2.1 代码自动生成 通过MyBatis-Plus提供的代码生成器来快速生成基础的 Controller、Service、Mapper 等模块，使用GenerateTest类来实现： package com.dust; import com.baomidou.mybatisplus.generator.AutoGenerator; import com.baomidou.mybatisplus.generator.config.DataSourceConfig; import com.baomidou.mybatisplus.generator.config.GlobalConfig; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.generator.config.PackageConfig; import com.baomidou.mybatisplus.generator.config.StrategyConfig; import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy; public class GenerateTest { public static void main(String[] args) { //创建generator对象 AutoGenerator autoGenerator = new AutoGenerator(); //数据源 DataSourceConfig dataSourceConfig = new DataSourceConfig(); dataSourceConfig.setDbType(DbType.MYSQL); dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver"); dataSourceConfig.setUsername("root"); dataSourceConfig.setPassword("102233"); dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/mbtest"); autoGenerator.setDataSource(dataSourceConfig); //全局配置 GlobalConfig globalConfig = new GlobalConfig(); globalConfig.setOutputDir(System.getProperty("user.dir")+"/src/main/java"); globalConfig.setAuthor("admin"); globalConfig.setOpen(false); globalConfig.setServiceName("%sService"); autoGenerator.setGlobalConfig(globalConfig); //包信息 PackageConfig packageConfig = new PackageConfig(); packageConfig.setParent("com.dust"); packageConfig.setEntity("entity");; packageConfig.setMapper("mapper"); packageConfig.setService("service"); packageConfig.setServiceImpl("service.impl"); packageConfig.setController("controller"); autoGenerator.setPackageInfo(packageConfig); //策略配置 StrategyConfig strategyConfig = new StrategyConfig(); strategyConfig.setInclude("fruit"); strategyConfig.setNaming(NamingStrategy.underline_to_camel); strategyConfig.setColumnNaming(NamingStrategy.underline_to_camel); strategyConfig.setEntityLombokModel(true); autoGenerator.setStrategy(strategyConfig); //运行 autoGenerator.execute(); } } 该代码生成器会根据数据库中的表结构自动生成实体类、Mapper、Service、Controller 等，极大提升开发效率。 2.2 分页插件配置 由于使用MyBatis-Plus构造器自动构造我们不用手动搭建各层结构。实体类直接依照数据自动能够创建。下面我们只需要编写以下代码： 1、引入MyBatis-Plus自带的分页器，确保分页查询的性能和简便性 在工具配置文件下新建一个类 PageConfiguration package com.dust.configuration; import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 使用mybatis-plus自带分页插件 */ @Configuration @MapperScan("com.dust.mapper") public class PageConfiguration { //注入mybatis-plus分页器PaginationInterceptor @Bean public PaginationInterceptor paginationInterceptor(){ return new PaginationInterceptor(); } } 2.3 Controller 层实现分页 在ProController中编写分页查询逻辑。使用 IPage 来封装分页结果，Page 对象用来接收分页参数： 2、Controller层 package com.dust.controller; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.dust.entity.Fruit; import com.dust.entity.LayuiPage; import com.dust.entity.Pro; import com.dust.service.ProService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.stereotype.Controller; import java.util.List; /** * * 前端控制器 * * * @author admin * @since 2022-02-24 */ @RestController @RequestMapping("/pro") public class ProController { @Autowired private ProService proService; @RequestMapping("/pagelist") @ResponseBody //或者是GetMapping("/pagelist") public LayuiPage pagelist(int page, int limit){ Page<Pro> pager=new Page<>(page,limit); IPage<Pro> proIPage =proService.page(pager,new QueryWrapper<>()); //调用layuipage构造的方法 //proIPage.getTotal()是指页面内数量 //proIPage.getRecords()是指页数据 return new LayuiPage(proIPage.getTotal(),proIPage.getRecords()); } } 这里使用了@RequestParam来接收分页参数 page 和 limit，并通过ProService 调用MyBatis-Plus 的 page 方法实现分页查询，返回的 LayuiPage封装了总记录数和分页数据。 2.4 LayuiPage 实体类 用LayuiPage来封装分页结果，适配前端 LayUI 表格的格式： 3、layuipage实体类 package com.dust.entity; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import java.util.List; @Data @AllArgsConstructor @NoArgsConstructor public class LayuiPage<T> { private int code; private String msg; private Long count; private List<T> data; //此构造函数只包含了两个参数，当然这两个参数是够用的，如果可以可以构造四个参数 public LayuiPage(Long count, List<T> data) { this.count = count; this.data = data; } } 3 前端代码模块 前端采用layui，界面表格可以去layui官网选择粘贴到templates下改一下table模块下的代码与实体类对应，并且还要更改跳转接口与Controller层controller类url一致。示例：