美颜相机已经成为当代年轻人不可或缺的自拍神器,其具有自动美肌,完美保留细节,让照片告别模糊等功能。或许我们会觉得编写这样一个具有如此强大功能的美颜相机一定需要庞大而且复杂的代码段,其实不然,即使对于初学者而言,也可以轻而易举的做出一款入门级的美颜相机。
一、思路分析
一款简易美颜相机的功能我们可以简单分为两大块:一是对拍摄出的照片可以进行马赛克,美白,灰度调整等功能;二是可以在照片上画圆圈,画直线等涂鸦功能。因此,我们在写代码时,把需要实现的功能分成如下两部分:
1、图像处理
2、画板
下面先展示本次完工后美颜相机的效果图:
二、代码分析
这边先只分析重要部分代码,最终的完整代码附在文章末尾!
和前面我们做五子棋游戏类似,在实现美颜相机功能的第一步还是先画界面,首先,我们需要画一个类似于 P 图软件的界面,上面拥有照片处理涂鸦区,按钮区,颜色选择区,菜单等功能区,因此先 new 一个 DrawUI 类(当然也可以取其他名字),在这个类中,我们首先把界面画出来,为了方便管理,我们设置了一个菜单和三个 JPanel,一个 JPanel 里放 ImagePanel,用于照片处理和涂鸦,另两个 Panel 里放相关功能按钮。
public void initUI(){
JFrame jf = new JFrame();
jf.setSize(1400,900);
jf.setTitle("PC版美颜相机");
jf.setLocationRelativeTo(null);
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
JPanel[] jps = addPanel(jf);
addButton(jps);
addJSlider(jps);
addMenu(jf);
}
添加界面上的 JPanel :
private JPanel[] addPanel(JFrame jf) {
JPanel[] jps = new JPanel[3];
String[] layoustrs = {BorderLayout.CENTER,BorderLayout.SOUTH,BorderLayout.EAST};
imgPanel.setBackground(Color.LIGHT_GRAY);
imgPanel.addMouseListener(ul);
jf.add(imgPanel,layoustrs[0]);
jps[0] = imgPanel;
Dimension dim = new Dimension(320,100);
JPanel jp_s1 = new JPanel();
jp_s1.setBackground(Color.ORANGE);
jp_s1.setPreferredSize(dim);
jf.add(jp_s1,layoustrs[1]);
jps[1] = jp_s1;
JPanel jp_s2 = new JPanel();
jp_s2.setBackground(Color.YELLOW);
jp_s2.setPreferredSize(dim);
jf.add(jp_s2,layoustrs[2]);
jps[2] = jp_s2;
return jps;
}
添加界面上的按钮:
private void addButton(JPanel[] jps) {
String[] btnstrs1 = new String[]{"原图","撤回","马赛克","灰度","轮廓","二值化","油画",
"美白","运动模糊","卷积","锐化","浮雕","左旋","右旋","放大","缩小","打开摄像头","关闭摄像头"};
for(int i = 0; i < btnstrs1.length-2; i++){
JButton btn = new JButton(btnstrs1[i]);
Dimension dim = new Dimension(120,40);
btn.setPreferredSize(dim);
btn.setFont(new Font("黑体",1,20));
btn.setBackground(Color.WHITE);
btn.addActionListener(ul);
jps[2].add(btn);
}
for(int i = btnstrs1.length-2; i < btnstrs1.length; i++){
JButton btn = new JButton(btnstrs1[i]);
Dimension dim = new Dimension(200,40);
btn.setPreferredSize(dim);
btn.setFont(new Font("黑体",1,20));
btn.setBackground(Color.WHITE);
btn.addActionListener(ul);
jps[2].add(btn);
}
String[] btnstrs2 = new String[] {"直线","矩形","椭圆","三角形","正方形","菱形","特殊三角形","多边形","画点","迭代","树"};
for(int i = 0; i < btnstrs2.length; i++) {
JButton btn = new JButton(btnstrs2[i]);
Dimension dim = new Dimension(120,40);
btn.setPreferredSize (dim);
btn.setFont (new Font ("黑体",1,16));
btn.setBackground (Color.WHITE);
btn.addActionListener (ul);
jps[1].add (btn);
}
Color []color= {Color.BLUE,Color.GREEN,Color.YELLOW,Color.BLACK,Color.ORANGE,
Color.GRAY,Color.RED};
for(int i=0;i<color.length;i++) {
JButton jbui=new JButton();
jbui.setBackground(color[i]);
Dimension dmi=new Dimension(40,40);
jbui.setPreferredSize(dmi);
jbui.addActionListener(ul);
jps[1].add(jbui);
}
}
添加界面上的滑动条:
private void addJSlider(JPanel[] jps) {
JLabel j0 = new JLabel(" ");
j0.setFont(new Font("黑体",1,30));
jps[2].add(j0);
JLabel j1 = new JLabel("大小/程度选择:");
j1.setFont(new Font("黑体",1,30));
jps[2].add(j1);
JSlider jSlider = new JSlider(0,100);
Dimension dm = new Dimension(300,80); //设置按钮尺寸
jSlider.setPreferredSize (dm);
jSlider.setFont (new Font ("黑体",1,15));
jSlider.setMajorTickSpacing(20);
jSlider.setMinorTickSpacing(5);
jSlider.setPaintLabels(true);//确定是否在滑块上绘制标签
jSlider.setPaintTicks(true);//确定是否在滑块上绘制刻度标记
jSlider.setPaintTrack(true);//确定是否在滑块上绘制滑道
jSlider.setSnapToTicks(true);//指定为 true,则滑块(及其所表示的值)解析为最靠近用户放置滑块处的刻度标记的值
jSlider.setOpaque(false);
jps[2].add(jSlider);
jSlider.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
System.out.println("当前值: " + jSlider.getValue());
int p = jSlider.getValue();
iu.p = p;
}
});
}
添加界面上的菜单:
private void addMenu(JFrame jf) {
//菜单栏
JMenuBar jmb = new JMenuBar();
jf.add(jmb,BorderLayout.NORTH);
//菜单选项
JMenu file = new JMenu("文件");
file.setPreferredSize(new Dimension(80,40));
file.setFont(new Font("宋体",2,22));
jmb.add(file);
//子菜单
JMenuItem jmi1 = new JMenuItem("打开");
jmi1.setPreferredSize(new Dimension(80,40));
jmi1.setFont(new Font("宋体",2,22));
jmi1.addActionListener(ul);
file.add(jmi1);
JMenuItem jmi2 = new JMenuItem("保存");
jmi2.setPreferredSize(new Dimension(80,40));
jmi2.setFont(new Font("宋体",2,22));
jmi2.addActionListener(ul);
file.add(jmi2);
jf.setVisible(true);
}
界面画完后就涉及到图像处理部分了,根据事先设定好的路径把图片的像素读进 imgArray[ ][ ] 数组中,后期根据需要对数组中每一个位置的值进行相应的处理变换就可以获得处理后的图像啦!代码中,width 是我们读取到的图像的宽度,height 是我们读取到的图像的高度,后面我们又设定了 width0 和 height0 的值,他们都等于图像宽度和高度中的较大者,其实, width0 和 height0 的值是我们定义的画布的宽度和高度,因为我们后面会用到图像旋转功能,所以画布的尺寸边长应为图像宽度和高度值中的较大者,(如果画布尺寸和图像尺寸一样,那么在旋转的过程中可能图像会显示不全)。
public int[][] readImagePix(String path){
// 文件对象
File file = new File (path);
BufferedImage readimg = null;
try {
readimg = ImageIO.read (file);
} catch (IOException e) {
e.printStackTrace ();
}
int width = readimg.getWidth ();
int height = readimg.getHeight ();
int height0 = 0;
int width0 = 0;
if(width >= height){
height0 = width;
width0 = width;
}
if(width < height){
width0 = height;
height0 = height;
}
imgPanel.width = width;
imgPanel.height= height;
int[][] imgArray = new int[width0][height0];
for(int i = 0; i < width; i++){
for(int j = 0; j < height; j++){
imgArray[i][j] = readimg.getRGB (i, j);
}
}
return imgArray;
}
为了后面处理图像方便,我们先写了一个获取灰度值的函数:
public int getRgbGray(int numPixels){
// byte -128 127
int red = (numPixels>>16)&0xFF;
int green = (numPixels>>8)&255;
int blue = (numPixels>>0)&255;
// 灰度 -- 减少计算量 以及 更方便计算
int gray = (red + green + blue) / 3;
return gray;
}
下面介绍几种图像处理的方法,第一个是原图,原图应该是最简单的部分了,我们只需要根据数组 imgArray[ ][ ] 把每一个位置的像素画出来就好。
public BufferedImage drawImage_00(int[][] imgArray){
BufferedImage buffimg = new BufferedImage ( imgArray.length, imgArray[0].length, BufferedImage.TYPE_INT_ARGB);
for(int i = 0; i < imgArray.length; i++){
for(int j = 0; j < imgArray[i].length; j++){
buffimg.setRGB (i,j,imgArray[i][j]);
}
}
return buffimg;
}
马赛克就是在图像上每隔一定的距离画一个方块,该方块的颜色和方块中心的颜色一样,当然也可以取方块中所有像素的平均值,这边需要注意的是,图像的长度并不一定是方块边长的整数倍,所以要注意图像边缘部分的处理。为了能够调整马赛克方块的大小,我们在界面上设置了一个滑块,可以调整滑块来调整马赛克程度:
public BufferedImage drawImage_01(int[][] imgArray){
BufferedImage buffimg = new BufferedImage ( imgArray.length, imgArray[0].length, BufferedImage.TYPE_INT_ARGB);
System.out.print("iup = "+p);
if(p < 20)p = 20;
//可使用fillRect
for(int i = p/10; i < imgArray.length; i += (2*(p/10)+1)){
for(int j = p/10; j < imgArray[i].length; j += (2*(p/10)+1)){
int mm,nn;
if(i+(p/10+1)>imgArray.length) {
mm = imgArray.length;
}else {
mm = i+(p/10+1);
}
if(j+(p/10+1)>imgArray[i].length) {
nn = imgArray[i].length;
}else {
nn = j+(p/10+1);
}
for(int m=i-p/10;m<mm;m++) {
for(int n=j-p/10;n<nn;n++) {
buffimg.setRGB (m, n, imgArray[i][j]);
}
}
}
}
for(int i = imgArray.length-(p/10+1); i < imgArray.length; i++) {
int a = imgArray[i].length/(p/10*2+1);
for(int m = 0; m < a; m++) {
for(int j = (p/10*2+1)*m; j < (p/10*2+1)*(m+1); j++){
buffimg.setRGB (i, j, imgArray[imgArray.length-1][(p/10*2+1)*m+(p/10+1)]);
}
}
}
int b = imgArray.length/(p/10*2+1);
for(int m = 0; m < b; m++) {
for(int i = (p/10*2+1)*m; i < (p/10*2+1)*(m+1); i++){
for(int j = imgArray[i].length-(p/10+1); j < imgArray[i].length; j++) {
buffimg.setRGB (i, j, imgArray[(p/10*2+1)*m+(p/10+1)][imgArray[i].length-1]);
}
}
}
for(int i = imgArray.length-(p/10+1); i < imgArray.length; i++){
for(int j = imgArray[i].length-(p/10+1); j < imgArray[i].length; j++) {
buffimg.setRGB (i, j, imgArray[imgArray.length-1][imgArray[i].length-1]);
}
}
return buffimg;
}
滑块值为50:
滑块值为100:
灰度处理直接调佣 getRgbGray 函数即可:
public BufferedImage drawImage_02(int[][] imgArray){
BufferedImage buffimg = new BufferedImage (imgArray.length, imgArray[0].length, BufferedImage.TYPE_INT_ARGB);
for(int i = 0; i < imgArray.length; i++){
for(int j = 0; j < imgArray[i].length; j++){
int num1 = imgArray[i][j];
int gray1 = getRgbGray (num1);
Color color = new Color (gray1,gray1,gray1);
buffimg.setRGB (i, j, color.getRGB ());
}
}
return buffimg;
}
轮廓处理首先需要识别轮廓,通过比较相邻的像素值,差距过大表示此处颜色差距大(此差距值可以通过人为调整滑块来决定),应该是轮廓边框,然后将此边缘设定为白色,其余部分设定为黑色即可:
public BufferedImage drawImage_03(int[][] imgArray){
BufferedImage buffimg = new BufferedImage (imgArray.length, imgArray[0].length, BufferedImage.TYPE_INT_ARGB);
for(int i = 0; i < imgArray.length - 1; i++){
for(int j = 1; j < imgArray[i].length - 1; j++){
int num1 = imgArray[i][j];
int num2 = imgArray[i + 1][j + 1];
int num3 = imgArray[i + 1][j - 1];
int num4 = imgArray[i + 1][j];
int num5 = imgArray[i][j + 1];
int gray1 = getRgbGray (num1);
int gray2 = getRgbGray (num2);
int gray3 = getRgbGray (num3);
int gray4 = getRgbGray (num4);
int gray5 = getRgbGray (num5);
// 比较相邻的像素值 差距过大表示此处颜色差距大 应该是轮廓边框
if((Math.abs (gray1 - gray2) > (7+p/10))||(Math.abs (gray1 - gray3) > (7+p/10))||(Math.abs (gray1 - gray4) > (7+p/10))||(Math.abs (gray1 - gray5) > (7+p/10))){
// 绘制为白色
buffimg.setRGB (i, j, Color.WHITE.getRGB ());
} else{
// 绘制为黑色
buffimg.setRGB (i, j, Color.BLACK.getRGB ());
}
}
}
return buffimg;
}
二值化处理只需判断该点像素值大小,并设定一个阈值(此阈值大小可以通过人为调整滑块来决定)大于该阈值将该点像素设定为白色,小于该阈值将该点像素设定为黑色:
public BufferedImage drawImage_04(int[][] imgArray){
BufferedImage buffimg = new BufferedImage (imgArray.length, imgArray[0].length, BufferedImage.TYPE_INT_ARGB);
for(int i = 0; i < imgArray.length; i++){
for(int j = 0; j < imgArray[i].length; j++){
int num1 = imgArray[i][j];
int gray1 = getRgbGray (num1);
if(gray1 > 78+p){
// 绘制为白色
buffimg.setRGB (i, j, Color.WHITE.getRGB ());
} else{
// 绘制为黑色
buffimg.setRGB (i, j, Color.BLACK.getRGB ());
}
}
}
return buffimg;
}
油画处理即在该图像上画不同大小的椭圆(椭圆的平均大小可以通过人为调整滑块来决定)即可:
public BufferedImage drawImage_05(int[][] imgArray){
BufferedImage buffimg = new BufferedImage (imgArray.length, imgArray[0].length, BufferedImage.TYPE_INT_ARGB);
Graphics bg = buffimg.getGraphics();
for(int i = (10+