2024年11月

救园成功后,我们一边开启
AI之旅
,一边尝试做一些当前力所能及的商业化项目,增加园子的收入来源。

这个月开始尝试做一些天翼云电脑的代理业务,先给大家赠送一些天翼云电脑体验一下。

赠送方案:

  • 终身PLUS会员赠送1年4核8G云电脑(限1个名额,已被领取)
  • 终身会员赠送3个月4核8G云电脑(限20个名额)
  • 年度会员赠送1个月4核8G云电脑(限50个名额)
  • 非会员赠送7天2核4G云电脑(分2批,每批限50个名额)

领取方法:

  • 第一位在这篇博文的评论中回复「领云电脑」的终身PLUS会员,可领1年4核8G云电脑
  • 其他人加园子的
    企业微信
    领取,加好友请备注【领云电脑】

发放方法:

  • 通过好友请求后,如果名额未满,会让您提供邮箱与登录云电脑的预设账号名以及区域(所在省份)
  • 如果是会员,1天内会安排发放;如果是非会员,等50个名额人满之后统一安排发放
  • 发放时天翼云官方邮箱会给您的邮箱发一封激活邮件,打开激活链接后设置密码并激活(可以不用填手机号)
    注:使用天翼云电脑,不需要在天翼云注册账号

购买特惠:

  • 11月期间通过园子购买,统一享受
    官网
    价1折
  • 11月之后通过园子购买,包月购买2折,包半年到1年购买1.8折,1年以上1.5折

购买方法:

  • 加园子的
    企业微信
    购买,加好友请备注【购买云电脑】

使用方法:

  • 默认安装的是 Windows Server 2019 标准版

  • 可以自己更换操作系统

欢迎体验云电脑!

增强 vw/rem 移动端适配,适配宽屏、桌面端、三折屏

vw 和 rem 是两个神奇的
CSS 长度
单位,认识它们之前,我一度认为招聘广告上的“像素级还原”是一种超能力,我想具备这种能力的人,一定专业过硬、有一双高分辨率的深邃大眼睛。

时间一晃,入坑两年,我敏捷地移动有点僵硬不算过硬的小手,将一些固定的 px 尺寸复制到代码,等待编译阶段的 vw/rem 转换,刷新浏览器的功夫,完美还原的界面映入眼前,我推了推眼镜,会心一笑。多亏了 vw 和 rem。

TLDR:极简配置
postcss-mobile-forever
增强 vw 的宽屏可访问性,限制视图最大宽度。

用 vw 和 rem 适配移动端视图的结果是一致的,都会得到一个随屏幕宽度变化的
等比例伸缩视图
。一般使用
postcss-px-to-viewport
做 vw 适配,使用
postcss-px2rem
配合
amfe-flexible
做 rem 适配。由于 rem 适配的原理是模仿 vw,所以后面关于适配的增强,一律使用 vw 适配做对比。

vw 适配有一些优点(同样 rem):

  • 加速开发效率;
  • 像素级还原设计稿;
  • 也许更容易被自动生成。

但是 vw 适配也不完美,它引出了下面的问题:

  1. 开发过程机械化,所有元素是固定的宽高,不考虑
    响应式设计
    ,例如不使用
    flex

    grid
    布局;
  2. 助长不规范化,不关注细节,只关注页面还原度和开发速度,例如在非按钮元素的
    <div>
    上添加点击事件;
  3. 桌面端难以访问,包容性降低。

前两个问题,也许要抛弃 vw、回归响应式布局才能解决,在日常开发时,我们要约束自己以开发桌面端的标准来开发移动端页面,改善这两个问题。

马克·吐温在掌握通过密西西比河的方法之后,发现这条河已经失去了它的美丽——总会丢掉一些东西,但是在艺术中比较不受重视的东西同时也被创造出来了。让我们不要再注意丢掉了什么,而是注意获得了什么。 ——《禅与摩托车的维修艺术》

后面,我们将关注第三点,介绍如何在保持现状(vw 适配)的情况下,尽可能提高不同屏幕的包容性,至少让我们在三折屏的时代能得到从前 1 倍的体验,而不是 1/3。

移动端 桌面端
一个展示正常的移动端页面 撑满屏幕而无法阅览的展示在桌面端的移动端页面

上面是
一个页面
分别在手机和电脑上展示的截图,可以看到左图移动端的右上角没有隐藏分享按钮,所以用户是允许(也应该允许)被分享到桌面端访问的,可惜,当用户准备享受大屏震撼的时候,真的被震撼了:他不知道这个页面的技术细节是神奇的 vw,也不知道他只能用鼠标小心地拖动浏览器窗口边缘,直到窗口窄得和手机一样,最崩溃的是,当他得意地按下了浏览器的缩小按钮,页面像冰冷的机器纹丝不动,浇灭了他的最后一点自信。

限制最大宽度

由于 vw 是视口单位,因此当屏幕变宽,vw 元素也会变大,无限变宽,无限变大。

现在假设在一张宽度 600 像素的设计图上,有一个宽度
60px
的元素,最终通过工具,它会被转为
10vw
。这个
10vw
元素是任意伸缩的,但是现在我希望,当屏幕宽度扩大到
700px
后,停止元素的放大。

出现了一堆枯燥的数字,不用担心,后面还有一波,请保持耐心。

首先计算
10vw
在宽 700 像素的屏幕上,应该是多少像素:60 * 700 / 600 = 70。通过最大宽度(
700px
)和标准宽度(
600px
)的比例,乘以元素在标准宽度时的尺寸(
60px
),得到了元素的最大尺寸
70px

接着结合 CSS 函数:
min(10vw, 70px)
,这样元素的宽度将被限制在
70px
以内,小于这个宽度时会以
10vw
等比伸缩。

除了上面的作为正数的尺寸,可能还会有用于方位的负数,负数的情况则使用 CSS 函数
max()
,下面的代码块是一个具体实现:

/**
 * 限制大小的 vw 转换
 * @param {number} n
 * @param {number} idealWidth 标准/设计稿/理想宽度,设计稿的宽度
 * @param {number} maxWidth 表示伸缩视图的最大宽度
 */
function maxVw(n, idealWidth = 600, maxWidth = 700) {
  if (n === 0) return n;

  const vwN = Math.round(n * 100 / idealWidth);
  const maxN = Math.round(n * maxWidth / idealWidth);
  const cssF = n > 0 ? "min" : "max";
  return `${cssF}(${vwN}vw, ${maxN}px)`;
}

矫正视图外的元素位置

上一节提供的方法,包容了限制最大宽度尺寸的大部分情况,但是如果不忘像素级还原的❤️初心,就会找到一些漏洞。

下面是一个图例,移动端页面提供了 Top 按钮用于帮助用户返回顶部,按照上一节的方法,Top 按钮会出现在中央移动端视图之外、右边的空白区域中,而不是矫正回中央移动端视图的右下角。

简笔图分成了左右两部分,左边指向右边,左边的部分包含一个视图和视图之外的 Top 按钮,右边的部分包含了一个视图和视图内的 Top 按钮

假设 Top 按钮的样式是这样的:

.top {
  position: fixed;
  right: 30px;
  bottom: 30px;
  /* ... */
}

按照标准宽度 600、最大宽度 700,上面的
30px
都被转换成了
min(5vw, 35px)

bottom
没错,但
right
需要矫正。

对照上面右图矫正过的状态,
right
的值
=
右半边的空白长度
+
Top 按钮到居中视图右边框的长度
=
桌面端视图的一半
-
视图中线到 Top 按钮的右边框长度。

沿着第二个等号后面的思路,
fixed
定位时桌面端视图一半的尺寸即为
50%
,中线到 Top 按钮右边框的长度,分两种情况:

  • 在屏幕宽度大于最大宽度 700 时,为 700 / 2 - 30 * 700 / 600,即为
    315px
    (其中 700 / 2 是中线到移动端右边框长度,30 * 700 / 600 是屏宽 600 时的
    30px
    在屏宽 700 时的尺寸);
  • 在屏幕宽度小于最大宽度 700 时,为 (600 / 2 - 30) / 600,即为
    45%

结合
calc()

min()
和上面得到的
50%

315px

45%
,参考第二个等式,可以得到
right
的新值为
calc(50% - min(315px, 45%))
。当尺寸大于移动端视图的一半时,会出现负数的情况,这时使用
max()
替换
min()

上面的计算方法是一种符合预期的稳定的方法,另一种方法是强制设置移动端视图的根元素成为
包含块
,设置之后,
right: min(5vw, 35px)
将不再基于浏览器边框,而是基于移动端视图的边框。

postcss-mobile-forever

上面介绍了增强 vw 以包容移动端视图在宽屏展示的两个方面,除了介绍的这些,还有一点点边角情况,例如:

  • 逻辑属性
    的判断和转换;
  • 矫正
    fixed
    定位时和包含块宽度有关的
    vw

    %
    尺寸;
  • 矫正
    fixed
    定位时
    left

    right

    vw

    %
    尺寸;
  • 为移动端视图添加居中样式;
  • 各种情况的判断和转换方法选择。

postcss-mobile-forever
是一个 PostCSS 插件,利用 mobile-forever 这些工作可以在编译阶段完成,上面举了那么多例子,汇总成一份 mobile-forever 配置就是:

{
  "viewportWidth": 700,
  "appSelector": "#app",
  "maxDisplayWidth": 600
}

上面是 mobile-forever 用户使用最多的模式,
max-vw-mode
,此外还提供:

  • mq-mode
    ,media-query 媒体查询模式,生成可访问性更高的样式,同样限制最大宽度,但是避免了
    vw
    带来的无法通过桌面端浏览器缩放按钮缩放页面的问题,也提供了更高的浏览器兼容性;
  • vw-mode
    ,朴素地将固定尺寸转为 vw 伸缩页面,不限制最大宽度。

postcss-mobile-forever 相比 postcss-px-to-viewport 提供了更多的模式,包容了宽屏展示,相比 postcss-px2rem,无需加载 JavaScript,不为项目引入复杂度,即使用户禁用了 js,也能正常展示页面。

scale-view
提供运行时的转换方法。

优秀的模版

postcss-mobile-forever 的推广离不开开源模版的支持、尝试与反馈,下面是这些优秀的模版,它们为开发者提供了更多元的选项,为用户提供了更包容的产品:

  • vue3-vant-mobile
    ,一个基于 Vue 3 生态系统的移动 web 应用模板,帮助你快速完成业务开发。【
    查看在线演示
  • vue3-vant4-mobile
    ,基于Vue3.4、Vite5、Vant4、Pinia、Typescript、UnoCSS等主流技术开发,集成 Dark Mode(暗黑)模式和系统主题色,且持久化保存,集成 Mock 数据,包括登录/注册/找回/keep-alive/Axios/useEcharts/IconSvg 等其他扩展。你可以在此之上直接开发你的业务代码!【
    查看在线演示
  • fantastic-mobile
    ,一款自成一派的移动端 H5 框架,支持多款 UI 组件库,基于 Vue3。【
    查看在线演示


一个在桌面端、移动端、平板上展示良好的网页

增强后的 vw/rem 看起来已经完成了适配宽屏的任务,不过回想最初的另外两个问题,机械化的开发过程与不规范化的开发细节,没有解决。作为一名专业的前端开发工程师,请考虑使用
响应式设计
开发你的下一个项目,为三折屏带来 3 倍的用户体验吧。

技术背景

关于增强采样(Enhanced Sampling)算法的具体原理,这里暂不做具体介绍,感兴趣的童鞋可以直接参考下这篇综述文章:
Enhanced sampling in molecular dynamics
。大致的作用就是,通过统计力学的方法,使得目标分子的CV(Collective Variables)具有一个尽可能大的采样子空间,并且可以将其还原回真实的自由能面。常用的增强采样算法,有早期的伞形采样,到后来大家常用的MetaDynamics以及高老师的温度积分增强采样算法(ITS)。在MindSponge中已经实现了MetaDynamics算法和ITS算法,本文我们使用MetaDynamics算法来做一个演示。

准备工作

使用MindSponge,可以参考
本系列文章
先了解一下MindSponge的安装和基本使用方法。这里我们用一个比较简单的多肽体系进行一个测试,相应的pdb文件为:

CRYST1    0.000    0.000    0.000  90.00  90.00  90.00 P 1           1
ATOM      1  H1  ACE A   1      -1.838  -6.570  -0.492  0.00  0.00      
     
ATOM      2  CH3 ACE A   1      -0.764  -6.587  -0.283  0.00  0.00      
     
ATOM      3  H2  ACE A   1      -0.392  -7.533  -0.746  0.00  0.00      
     
ATOM      4  H3  ACE A   1      -0.592  -6.446   0.740  0.00  0.00      
     
ATOM      5  C   ACE A   1      -0.006  -5.404  -0.828  0.00  0.00      
     
ATOM      6  O   ACE A   1      -0.544  -4.619  -1.673  0.00  0.00      
     
ATOM      7  N   ALA A   2       1.278  -5.323  -0.423  0.00  0.00      
     
ATOM      8  H   ALA A   2       1.622  -5.845   0.368  0.00  0.00      
     
ATOM      9  CA  ALA A   2       2.284  -4.164  -0.399  0.00  0.00      
     
ATOM     10  HA  ALA A   2       2.098  -3.653   0.505  0.00  0.00      
     
ATOM     11  CB  ALA A   2       3.651  -4.787  -0.566  0.00  0.00      
     
ATOM     12  HB1 ALA A   2       4.274  -4.031  -0.972  0.00  0.00      
     
ATOM     13  HB2 ALA A   2       3.977  -5.106   0.419  0.00  0.00      
     
ATOM     14  HB3 ALA A   2       3.697  -5.612  -1.274  0.00  0.00      
     
ATOM     15  C   ALA A   2       1.995  -3.152  -1.576  0.00  0.00      
     
ATOM     16  O   ALA A   2       1.544  -2.065  -1.221  0.00  0.00      
     
ATOM     17  N   NME A   3       2.255  -3.614  -2.845  0.00  0.00      
     
ATOM     18  H   NME A   3       2.788  -4.485  -2.929  0.00  0.00      
     
ATOM     19  CH3 NME A   3       1.991  -2.802  -4.055  0.00  0.00      
     
ATOM     20 HH31 NME A   3       2.561  -1.891  -3.988  0.00  0.00      
     
ATOM     21 HH32 NME A   3       1.897  -3.419  -4.937  0.00  0.00      
     
ATOM     22 HH33 NME A   3       0.985  -2.388  -3.930  0.00  0.00      

END

该构象可以保存成pdb文件然后直接用VMD进行可视化:

其他的力场文件我们直接使用MindSponge已经自带的
amber.ff14sb
力场即可。

普通MD案例

在测试增强采样算法之前,我们可以先跑一段普通的分子模拟测试一下:

from mindspore import nn, context
import mindspore as ms
# 固定随机种子,确保朗之万控温的随机数可以被复现
ms.set_seed(0)
# 这里使用的是一个未编译版本的mindsponge,所以要把sponge所在路径添加到系统路径中
import sys
sys.path.insert(0, '../..')
from sponge import ForceField, Sponge, set_global_units, Protein, UpdaterMD, WithEnergyCell
from sponge.callback import RunInfo, WriteH5MD
from sponge.colvar import Torsion
from sponge.function import PI

context.set_context(mode=context.GRAPH_MODE, device_target='GPU')
set_global_units('A', 'kcal/mol')

system = Protein('mol.pdb', template=['protein0.yaml'], rebuild_hydrogen=False)
energy = ForceField(system, parameters=['AMBER.FF14SB'], use_pme=False)
min_opt = nn.Adam(system.trainable_params(), 1e-03)

dihedrals = Torsion([[4, 6, 8, 14], [6, 8, 14, 16]])
run_info = RunInfo(20)

opt = UpdaterMD(system=system,
                time_step=1e-3,
                integrator='velocity_verlet',
                temperature=300,
                thermostat='langevin')

sim = WithEnergyCell(system, energy)
md = Sponge(sim, optimizer=opt, metrics={'dihedrals': dihedrals})
cb_h5md = WriteH5MD(system, 'test_meta.h5md', save_freq=10, write_image=False)
md.run(5000, callbacks=[run_info, cb_h5md])

运行之后会在本地保存一个h5md格式的轨迹文件,可以在vscode中使用h5web拓展工具打开:

找到dihedral的value,用matrix的形式查看,然后可以导出csv格式(如:
path_sink.csv
):

导出csv之后,可以使用如下的python脚本进行绘图:

import numpy as np
import matplotlib.pyplot as plt

def gaussian2(x1, x2, sigma1=1.0, sigma2=1.0, A=0.5):
    return np.sum(A*np.exp(-0.5*(x1**2/sigma1**2+x2**2/sigma2**2))/np.pi/sigma1/sigma2, axis=-1)

def potential_energy(position, psi, phi, sigma1, sigma2):
    # (A, )
    psi_, phi_ = position[:, 0], position[:, 1]
    # (A, R)
    delta_psi = psi_[:, None] - psi[None]
    delta_phi = phi_[:, None] - phi[None]
    # (A, )
    Z = -np.log(gaussian2(delta_psi, delta_phi, sigma1=sigma1, sigma2=sigma2, A=2.0)+1)
    return Z

data = np.genfromtxt('./path_sink.csv', delimiter=',')
phi = data[:, 0]
psi = data[:, 1]

num_grids = 100
num_levels = 10
psi_grids = np.linspace(-np.pi, np.pi, num_grids)
phi_grids = np.linspace(-np.pi, np.pi, num_grids)
grids = np.array(np.meshgrid(psi_grids, phi_grids)).T.reshape((-1, 2))

Z = potential_energy(grids, phi, psi, 1.0, 1.0).reshape((psi_grids.shape[0], phi_grids.shape[0])).T
X,Y = np.meshgrid(psi_grids, phi_grids)
levels = np.linspace(np.min(Z), np.max(Z), num_levels)

plt.figure()
plt.title("Biased MD Traj")
plt.xlabel(r'$\phi$')
plt.ylabel(r'$\psi$')
fc = plt.contourf(X, Y, Z, cmap='Greens', levels=levels)
plt.colorbar(fc)

plt.xlim(-np.pi, np.pi)
plt.ylim(-np.pi, np.pi)
plt.plot(phi, psi, 'o', alpha=0.4, color='red')
plt.savefig('meta.png')

画出来的轨迹效果如下图所示:

可以发现,在不加任何的Bias的时候,整个轨迹还是比较集中的在一个区域,一般该区域就是对应了一个能量极小值点附近的区域。

MetaDynamics案例

从上一个章节的结果中可以看出,常规的分子模拟采样方法很容易陷入到一个局部区域中,这样使得我们很难可以观测到其他采样子空间所对应的构象和相关信息,因此我们可以引入一个MetaDynamics增强采样算法,做一个有偏估计:

from mindspore import nn, context
import mindspore as ms
ms.set_seed(0)
import sys
sys.path.insert(0, '../..')
from sponge import ForceField, Sponge, set_global_units, Protein, UpdaterMD, WithEnergyCell
from sponge.callback import RunInfo, WriteH5MD
from sponge.colvar import Torsion
from sponge.function import PI
from sponge.sampling import Metadynamics

context.set_context(mode=context.GRAPH_MODE, device_target='GPU')
set_global_units('A', 'kcal/mol')

system = Protein('mol.pdb', template=['protein0.yaml'], rebuild_hydrogen=False)
energy = ForceField(system, parameters=['AMBER.FF14SB'], use_pme=False)
min_opt = nn.Adam(system.trainable_params(), 1e-03)

# 这里定义的CV是phi和psi角,是一个2维的CV
dihedrals = Torsion([[4, 6, 8, 14], [6, 8, 14, 16]])
run_info = RunInfo(20)

# 配置Meta的参数,主要是高斯波包的高度、宽度、更新频率、CV范围、CV格点数等等
metad = Metadynamics(
    colvar=dihedrals,
    update_pace=10,
    height=2.5,
    sigma=0.4,
    grid_min=-PI,
    grid_max=PI,
    grid_bin=50,
    temperature=300,
    bias_factor=100,
    use_cutoff=True,
)

opt = UpdaterMD(system=system,
                time_step=1e-3,
                integrator='velocity_verlet',
                temperature=300,
                thermostat='langevin')

sim = WithEnergyCell(system, energy, bias=metad)
md = Sponge(sim, optimizer=opt, metrics={'dihedrals': dihedrals})
cb_h5md = WriteH5MD(system, 'test_meta.h5md', save_freq=10, write_image=False)
md.run(5000, callbacks=[run_info, cb_h5md])

用类似的方法,可以计算得增强采样之后的CV轨迹:

可以看到,采样子空间在Meta的作用下已经扩展到几乎整个CV空间,这使得我们可以更快的去分析整个采样空间各处的自由能的相对大小。

总结概要

本文介绍了在MindSponge中进行分子动力学模拟以及增强采样的实现方法。通过使用MetaDynamics增强采样算法,我们可以将分子模拟的采样子空间,从某个能量极小值区域,扩大到尽可能大的采样子空间。

版权声明

本文首发链接为:
https://www.cnblogs.com/dechinphy/p/mindsponge-meta.html

作者ID:DechinPhy

更多原著文章:
https://www.cnblogs.com/dechinphy/

请博主喝咖啡:
https://www.cnblogs.com/dechinphy/gallery/image/379634.html

记得之前在别的网站上看到这个喜庆的春节灯笼效果,觉得非常吸引人。虽然网上有一些现成的API可以直接实现,比如这个
春节灯笼API
,但使用后我发现两个问题:一是手机端访问时灯笼没有自适应,二是灯笼上的“春节快乐”四个字不能自定义。

为了解决这些问题,我找到了
这篇文章
,并“借鉴”了其中的源代码,稍加修改后转换成JavaScript方式引入使用。下面有完整的JS代码。

原文可查看效果:
张苹果博客灯笼效果

一,引入使用

距离春节还有不到90天,赶紧试试吧!将以下JS在您的页面直接引入即可。

   <!-- 将以下js放入您的页面即可。text为灯笼文字,默认:新年快乐 -->
   <script src="https://www.vae.zhangweicheng.xyz/web/denglong.js?text=好好学习"> </script>

二,完整JS代码

 // 张苹果博客:https://zhangpingguo.com/
 // 创建并添加元素
function createDengContainer() {
    const container = document.createElement('div');
    container.className = 'deng-container';

    // 从当前脚本的 URL 获取参数
    const scriptSrc = document.currentScript.src;
    const urlParams = new URLSearchParams(scriptSrc.split('?')[1]); // 获取 '?'
    const customText = urlParams.get('text'); // 获取参数名为'text'的值

    // 将获取的文本分割为字符数组,如果没有提供文本,则使用默认的“新年快乐”
    const texts = customText ? customText.split('') : ['新', '年', '快', '乐'];

    texts.forEach((text, index) => {
        const box = document.createElement('div');
        box.className = `deng-box deng-box${index + 1}`;

        const deng = document.createElement('div');
        deng.className = 'deng';

        const xian = document.createElement('div');
        xian.className = 'xian';

        const dengA = document.createElement('div');
        dengA.className = 'deng-a';

        const dengB = document.createElement('div');
        dengB.className = 'deng-b';

        const dengT = document.createElement('div');
        dengT.className = 'deng-t';
        dengT.textContent = text;

        dengB.appendChild(dengT);
        dengA.appendChild(dengB);
        deng.appendChild(xian);
        deng.appendChild(dengA);

        const shuiA = document.createElement('div');
        shuiA.className = 'shui shui-a';

        const shuiC = document.createElement('div');
        shuiC.className = 'shui-c';
        const shuiB = document.createElement('div');
        shuiB.className = 'shui-b';

        shuiA.appendChild(shuiC);
        shuiA.appendChild(shuiB);
        deng.appendChild(shuiA);
        box.appendChild(deng);
        container.appendChild(box);
    });

    document.body.appendChild(container);
}

// 添加CSS样式
function addStyles() {
    const style = document.createElement('style');
    style.type = 'text/css';
    style.textContent = `
        .deng-container {
            position: relative;
            top: 10px;
            opacity: 0.9;
            z-index: 9999;
            pointer-events: none;
        }
        .deng-box {
            position: fixed;
            right: 10px;
        }
        .deng-box1 { position: fixed; top: 15px; left: 20px; }
        .deng-box2 { position: fixed; top: 12px; left: 130px; }
        .deng-box3 { position: fixed; top: 10px; right: 110px; }
        .deng {
            position: relative;
            width: 120px;
            height: 90px;
            background: rgba(216, 0, 15, .8);
            border-radius: 50% 50%;
            animation: swing 3s infinite ease-in-out;
            box-shadow: -5px 5px 50px 4px #fa6c00;
        }
        .deng-a { 
            width: 100px; 
            height: 90px; 
            background: rgba(216, 0, 15, .1); 
            border-radius: 50%;  
            border: 2px solid #dc8f03; 
            margin-left: 7px; 
            display: flex; 
            justify-content: center; 
        }
        .deng-b { 
            width: 65px; 
            height: 83px; 
            background: rgba(216, 0, 15, .1); 
            border-radius: 60%; 
            border: 2px solid #dc8f03; 
        }
        .xian { 
            position: absolute; 
            top: -20px; 
            left: 60px; 
            width: 2px; 
            height: 20px; 
            background: #dc8f03; 
        }
        .shui-a { 
            position: relative; 
            width: 5px; 
            height: 20px; 
            margin: -5px 0 0 59px; 
            animation: swing 4s infinite ease-in-out; 
            transform-origin: 50% -45px; 
            background: orange; 
            border-radius: 0 0 5px 5px; 
        }
        .shui-b { 
            position: absolute; 
            top: 14px; 
            left: -2px; 
            width: 10px; 
            height: 10px; 
            background: #dc8f03; 
            border-radius: 50%; 
        }
        .shui-c { 
            position: absolute; 
            top: 18px; 
            left: -2px; 
            width: 10px; 
            height: 35px; 
            background: orange; 
            border-radius: 0 0 0 5px; 
        }
        .deng:before, .deng:after { 
            content: " "; 
            display: block; 
            position: absolute; 
            border-radius: 5px; 
            border: solid 1px #dc8f03; 
            background: linear-gradient(to right, #dc8f03, orange, #dc8f03, orange, #dc8f03); 
        }
        .deng:before { 
            top: -7px; left: 29px; height: 12px; width: 60px; z-index: 999; 
        }
        .deng:after { 
            bottom: -7px; left: 10px; height: 12px; width: 60px; margin-left: 20px; 
        }
        .deng-t { 
            font-family: '华文行楷', Arial, Lucida Grande, Tahoma, sans-serif; 
            font-size: 3.2rem; 
            color: #dc8f03; 
            font-weight: 700; 
            line-height: 85px; 
            text-align: center; 
        }
        @media (max-width: 768px) { 
            .deng-t { font-size: 2.5rem; }  
            .deng-box { transform: scale(0.5); top: -15px; }  
            .deng-box1 { left: -22%; }  
            .deng-box2 { left: 0px; }  
            .deng-box3 { right: 50px; }  
            .deng-box4 { right: -10px; }  
        }
        @keyframes swing { 
            0% { transform: rotate(-10deg); }  
            50% { transform: rotate(10deg); }  
            100% { transform: rotate(-10deg); }  
        }
    `;
    document.head.appendChild(style);
}

// 引入时调用
function init() {
    addStyles();
    createDengContainer();
}

// 调用初始化函数
init();

三,页面使用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>春节灯笼</title>
</head>
<body>
    <!-- 引入js即可,text为灯笼文字,默认:新年快乐 -->
   <script src="./denglong.js?text=等着下班"> </script>
 </body></html>

四,效果图

一、介绍
今天是这个系列《C++之 Opencv 入门到提高》得第三篇文章。今天这篇文章也不难,主要介绍如何使用 Opencv 对图像进行掩膜处理,提高图像的对比度。在这个过程中,我们可以学到如何获取图像指针、如何处理像素值越界等问题。我们一步一个脚印的走,收获就会越来越多。虽然操作很简单,但是要下功夫理解每个技术点,把基础打扎实,才能让以后得学习过程更顺利一点。OpenCV 具体的简介内容,我就不多说了,网上很多,大家可以自行脑补。
OpenCV 的官网地址:
https://opencv.org/
,组件下载地址:
https://opencv.org/releases/

OpenCV 官网学习网站:
https://docs.opencv.ac.cn/4.10.0/index.html

我需要进行说明,以防大家不清楚,具体情况我已经罗列出来。
操作系统:Windows Professional 10(64位)
开发组件:OpenCV – 4.10.0
开发工具:Microsoft Visual Studio Community 2022 (64 位) - Current版本 17.8.3
开发语言:C++(VC16)

二、实例学习
这一节的内容不多,接口 API 其实也是挺简单的。但是我们学习接口,不能冷冰冰的只是学API、一些基础知识也是要掌握的。
图像中的掩膜(Mask)是什么?在图像处理中,掩膜(Mask)是一种用于控制图像处理区域或处理过程的特殊图像。它通常是一个与原始图像同样大小的二维矩阵,用于选择性地遮盖或显示图像的特定区域。掩膜可以用于多种图像处理任务,如图像分割、特征提取、增强等。
在数字图像处理中,掩膜通常是一个二进制图像,其中像素值为1的区域表示要保留的区域,像素值为0的区域表示要排除的区域。通过将掩膜与原始图像进行逻辑运算,可以创建新的图像,其中只有掩膜中标记为1的区域被保留,其他区域被排除。
掩膜在图像处理中有多种应用。例如,在图像分割中,掩膜可用于选择性地突出显示感兴趣的区域,以便进一步处理或分析。在特征提取中,掩膜可用于提取图像中的特定形状或结构。此外,掩膜还可以用于图像增强,例如通过模糊或锐化特定区域来改善图像质量。

数字图像处理中,掩模为二维矩阵数组,有时也用多值图像,图像掩模主要用于:
①提取感兴趣区,用预先制作的感兴趣区掩模与待处理图像相乘,得到感兴趣区图像,感兴趣区内图像值保持不变,而区外图像值都为0。
②屏蔽作用,用掩模对图像上某些区域作屏蔽,使其不参加处理或不参加处理参数的计算,或仅对屏蔽区作处理或统计。
③结构特征提取,用相似性变量或图像匹配方法检测和提取图像中与掩模相似的结构特征。
④特殊形状图像的制作。

什么是位图与掩膜的与运算?
其实就是原图中的每个像素和掩膜中的每个对应像素进行与运算。比如1 & 1 = 1;1 & 0 = 0;
比如一个3 * 3的图像与3 * 3的掩膜进行运算,得到的结果图像就是:

说白了,我们使用掩膜(mask)位图来选择哪个像素允许拷贝,哪个像素不允许拷贝。如果mask像素的值是非0的,我就拷贝它,也就是保留下,否则不拷贝,不保留。

在所有图像基本运算的操作函数中,凡是带有掩膜(mask)的处理函数,其掩膜都参与运算(输入图像运算完之后再与掩膜图像或矩阵运算)。

1 #include <opencv2/opencv.hpp>
2 #include <iostream>
3 #include <math.h>
4 
5 using namespacestd;6 using namespacecv;7 
8 
9 intmain()10 {11 Mat src, dst;12     src = imread("D:\\360MoveData\\Users\\Administrator\\Desktop\\TestImage\\4.jpg", IMREAD_UNCHANGED);13     if (!src.data)14 {15         cout << "图片加载错误!!!" <<endl;16         return -1;17 }18 
19     namedWindow("原始图像", WINDOW_AUTOSIZE);20     imshow("原始图像", src);21 
22     //1、获取图形像素指针23     //CV_Assert(myImage.depth()==CV_8U);24     //Mat.ptr<uchar>(int i=0)获取像素矩阵的指针,索引 i 表示第几行,从0开始计数。25     //获取当前行指针 const uchar* current=myImage.ptr<uchar>(row);26     //获取当前像素点 p(row,col) 的像素值 p(row,col)=current[col];27 
28     //2、像素范围处理 saturate_cast<uchar>29     //saturate_cast<uchar>(-100),返回 0.30     //saturate_cast<uchar>(288),返回 255,31     //saturate_cast<uchar>(100),返回 100.32     //这个函数的功能是确保 RGB 值的范围在 0-255 之间。
33 
34     double startDate =getTickCount();35 
36     //第一种实现对比度
37     /*int cols = (src.cols - 1) * src.channels();38 int offerts = src.channels();39 int rows = src.rows;40 
41 dst = Mat(src.size(), src.type());42 
43 for (int row = 1; row < (rows - 1); row++)44 {45 const uchar* previous = src.ptr<uchar>(row - 1);46 const uchar* current = src.ptr<uchar>(row);47 const uchar* next = src.ptr<uchar>(row + 1);48 uchar* output = dst.ptr<uchar>(row);49 for (int col = offerts; col < cols; col++)50 {51 output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offerts] + current[col + offerts] + previous[col] + next[col]));52 }53 }*/
54 
55     //3、定义掩膜56     //Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);57     //filter2D(src, dst, src.depth(), kernel):src 是原图,dst 是目标图,src.depth() 表示位图深度,有 32,24,8 等。58 
59     //第二种实现对比度
60     Mat kernel = (Mat_<char>(3, 3) << 0, -1, 0, -1, 5, -1, 0, -1, 0);61 filter2D(src, dst, src.depth(), kernel);62 
63     double totalTime = (getTickCount() - startDate) /getTickFrequency();64 
65     cout << "消费时间:"<< totalTime <<endl;66 
67     namedWindow("对比度图像", WINDOW_AUTOSIZE);68     imshow("对比度图像", dst);69 
70 
71     waitKey(0);72 
73     return 0;74 }

生成的效果图如下:

当然了,在源码中,提供了两种实现,效果都是一样的。一种是自己实现的,一种是通过调用接口实现的。这也说明了一个问题,只要你掌握的够深入,和接口效果一样的问题,你也可以写得出。

如果我们吧注释的代码打开,把【第二种实现】注释掉,源码:

1 int cols = (src.cols - 1) *src.channels();2 int offerts =src.channels();3 int rows =src.rows;4 
5 dst =Mat(src.size(), src.type());6 
7 for (int row = 1; row < (rows - 1); row++)8 {9     const uchar* previous = src.ptr<uchar>(row - 1);10     const uchar* current = src.ptr<uchar>(row);11     const uchar* next = src.ptr<uchar>(row + 1);12     uchar* output = dst.ptr<uchar>(row);13     for (int col = offerts; col < cols; col++)14 {15         output[col] = saturate_cast<uchar>(5 * current[col] - (current[col - offerts] + current[col + offerts] + previous[col] +next[col]));16 }17 }

而且,自己写的性能更好。对比如图:

再看看我们自己写的运行时间:

我们自己写实现的效果:



三、总结
这是 C++ 使用 OpenCV 的第三篇文章,概念挺难懂的,其实操作起来也没那么难,当然了,这只是我们入门的开始,那就继续吧。皇天不负有心人,不忘初心,继续努力,做自己喜欢做的,开心就好。