本文共 21327 字,大约阅读时间需要 71 分钟。
UI掌握PS这一逆天的软件,可以实现将图片转化为素描或者水彩的效果,以为例:
我们将以上四步进行抽象,得到将图片转为素描效果的步骤即为:
在Android图像处理领域,我们可以使用像素点分析的方法实现上述的效果组合。下面,用代码实现上述过程
1)去色,获取黑白图;
public static int[] discolor(Bitmap bitmap) { int picHeight = bitmap.getHeight(); int picWidth = bitmap.getWidth(); int[] pixels = new int[picWidth * picHeight]; bitmap.getPixels(pixels, 0, picWidth, 0, 0, picWidth, picHeight); for (int i = 0; i < picHeight; ++i) { for (int j = 0; j < picWidth; ++j) { int index = i * picWidth + j; int color = pixels[index]; int r = (color & 0x00ff0000) >> 16; int g = (color & 0x0000ff00) >> 8; int b = (color & 0x000000ff); int grey = (int) (r * KR + g * KG + b * KB); pixels[index] = grey << 16 | grey << 8 | grey | 0xff000000; } } return pixels; }
2)反相,得到图片的底图;
public static int[] reverseColor(int[] pixels) { int length = pixels.length; int[] result = new int[length]; for (int i = 0; i < length; ++i) { int color = pixels[i]; int r = 255 - (color & 0x00ff0000) >> 16; int g = 255 - (color & 0x0000ff00) >> 8; int b = 255 - (color & 0x000000ff); result[i] = r << 16 | g << 8 | b | 0xff000000; } return result; }
3)高斯模糊,得到反高斯图像;
public static void gaussBlur(int[] data, int width, int height, int radius, float sigma) { float pa = (float) (1 / (Math.sqrt(2 * Math.PI) * sigma)); float pb = -1.0f / (2 * sigma * sigma); // generate the Gauss Matrix float[] gaussMatrix = new float[radius * 2 + 1]; float gaussSum = 0f; for (int i = 0, x = -radius; x <= radius; ++x, ++i) { float g = (float) (pa * Math.exp(pb * x * x)); gaussMatrix[i] = g; gaussSum += g; } for (int i = 0, length = gaussMatrix.length; i < length; ++i) { gaussMatrix[i] /= gaussSum; } // x direction for (int y = 0; y < height; ++y) { for (int x = 0; x < width; ++x) { float r = 0, g = 0, b = 0; gaussSum = 0; for (int j = -radius; j <= radius; ++j) { int k = x + j; if (k >= 0 && k < width) { int index = y * width + k; int color = data[index]; int cr = (color & 0x00ff0000) >> 16; int cg = (color & 0x0000ff00) >> 8; int cb = (color & 0x000000ff); r += cr * gaussMatrix[j + radius]; g += cg * gaussMatrix[j + radius]; b += cb * gaussMatrix[j + radius]; gaussSum += gaussMatrix[j + radius]; } } int index = y * width + x; int cr = (int) (r / gaussSum); int cg = (int) (g / gaussSum); int cb = (int) (b / gaussSum); data[index] = cr << 16 | cg << 8 | cb | 0xff000000; } } // y direction for (int x = 0; x < width; ++x) { for (int y = 0; y < height; ++y) { float r = 0, g = 0, b = 0; gaussSum = 0; for (int j = -radius; j <= radius; ++j) { int k = y + j; if (k >= 0 && k < height) { int index = k * width + x; int color = data[index]; int cr = (color & 0x00ff0000) >> 16; int cg = (color & 0x0000ff00) >> 8; int cb = (color & 0x000000ff); r += cr * gaussMatrix[j + radius]; g += cg * gaussMatrix[j + radius]; b += cb * gaussMatrix[j + radius]; gaussSum += gaussMatrix[j + radius]; } } int index = y * width + x; int cr = (int) (r / gaussSum); int cg = (int) (g / gaussSum); int cb = (int) (b / gaussSum); data[index] = cr << 16 | cg << 8 | cb | 0xff000000; } } }
4)淡化颜色,生成Sketch图
public static void colorDodge(int[] baseColor, int[] mixColor) { for (int i = 0, length = baseColor.length; i < length; ++i) { int bColor = baseColor[i]; int br = (bColor & 0x00ff0000) >> 16; int bg = (bColor & 0x0000ff00) >> 8; int bb = (bColor & 0x000000ff); int mColor = mixColor[i]; int mr = (mColor & 0x00ff0000) >> 16; int mg = (mColor & 0x0000ff00) >> 8; int mb = (mColor & 0x000000ff); int nr = colorDodgeFormular(br, mr); int ng = colorDodgeFormular(bg, mg); int nb = colorDodgeFormular(bb, mb); baseColor[i] = nr << 16 | ng << 8 | nb | 0xff000000; } } private static int colorDodgeFormular(int base, int mix) { int result = base + (base * mix) / (255 - mix); result = result > 255 ? 255 : result; return result; }
5)将上述代码封如工具类中,最后定义一个获取素描图的方法
public static Bitmap testGaussBlur(Bitmap src, int r, int fai) { int width = src.getWidth(); int height = src.getHeight(); int[] pixels = Sketch.discolor(src); int[] copixels = Sketch.simpleReverseColor(pixels); Sketch.simpleGaussBlur(copixels, width, height, r, fai); Sketch.simpleColorDodge(pixels, copixels); Bitmap bitmap = Bitmap.createBitmap(pixels, width, height, Config.RGB_565); return bitmap; }
外界,调用时,传入两个int类型的参数即可,表示高斯模糊的程度和半径。
public class SketchActivity extends AppCompatActivity { private static final String TAG = "SketchActivity"; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_sketch); DisplayMetrics displayMetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); int width = displayMetrics.widthPixels; int height = displayMetrics.heightPixels; ImageView imageView = findViewById(R.id.test_img); Bitmap bitmap = BitmapFactory.decodeResource(getResources(),R.drawable.timg); // 对大图进行压缩 传入的参数为手机屏幕的尺寸 工具类在文末给出 bitmap = Utils.compressBySampleSize(bitmap, width, height, false); Bitmap bitmap = SketchUtil.testGaussBlur(bitmap,10,10); imageView.setImageBitmap(bitmap); // 素描图 } @Override protected void onDestroy() { super.onDestroy(); }}
效果如下:
在上面的算法中,我们为testGaussBlur()传入两个参数,即可实现调节素描的效果。现在提出另一个思路,就是在不改变已传入参数的前提下,调节素描的浓度:
要求:手指左滑浓度变淡,即素描图变透明;手指右滑变浓,即凸显素描图。
① 我们在原有ImageView的位置放入一个FrameLayout,然后在这个FrameLayout中放入两个ImageView,一个用于放置原图,在底层;一个用于放置素描效果图,在上层。两个ImageView的大小设为一样大,由于FrameLayout的特性,我们只能看见效果图。
② 然后我们为上层放置效果图的ImageView写入手势控制事件,当用户在图片上左右移动手指的时候,就调节上层图片的透明度,这样就达到了类似调节素描浓度的效果。
实现如下:
/** * 作者 cpf * 时间 2019/4/16 * 文件 TestApplication * 描述 手指在图片上滑动 可调节图片的透明度 * ①使用Seekbar是否可以做到? 没有办法区分左右滑动 记录上次滑动结果可以做的到 在onStopTrackingTouch方法中记录 * ②自定义View + 手势移动 onFling没法表示中间的过程 只有起始和结束时的状态 ,需要一个渐变的距离 onScroll */public class SeekbarActivity extends AppCompatActivity implements View.OnTouchListener { private static final String TAG = "SeekbarActivity"; private ImageView mImageView, mGestureImageView, originImageView; private SeekBar mSeekBar; private TextView mTxt, mGesTxt; private int mProgress = 100; private float mDistance = 1, maxDistance = 100; // 控制progress参数在0-100,0为完全透明 100为起始值可见 private GestureDetector mGestureDetector; private Bitmap finalBitmap,bitmap; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_seekbar); DisplayMetrics displayMetrics = new DisplayMetrics(); getWindowManager().getDefaultDisplay().getMetrics(displayMetrics); int width = displayMetrics.widthPixels; int height = displayMetrics.heightPixels; mImageView = findViewById(R.id.alpha_img); mGestureImageView = findViewById(R.id.ges_alpha_img); originImageView = findViewById(R.id.origin_img); mSeekBar = findViewById(R.id.alpha_seek); mTxt = findViewById(R.id.alpha_txt); mGesTxt = findViewById(R.id.ges_alpha_txt); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.timg); bitmap = Utils.compressBySampleSize(bitmap, width, height, false); finalBitmap = SketchUtil.testGaussBlur(bitmap,10,10); mImageView.setImageBitmap(bitmap); mGestureImageView.setImageBitmap(finalBitmap); originImageView.setImageBitmap(bitmap); mSeekBar.setMax(100); // 100 代表完不全透明 0 代表完全透明 mSeekBar.setProgress(100); mSeekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { Log.d(TAG, "onProgressChanged: direction..." + direction); if (direction == 0) { // 左滑 变透明 if ((mProgress - progress) > 0 && (mProgress - progress) < 100) { mImageView.setImageBitmap(SketchUtil.testGaussBlur(Utils.setAlpha(finalBitmap, (mProgress - progress)),10,10)); mTxt.setText(String.valueOf((mProgress - progress)) + "%"); Log.d(TAG, "onProgressChanged: 左滑," + (mProgress - progress)); } else if ((mProgress - progress) < 0) { mImageView.setImageBitmap(Utils.setAlpha(SketchUtil.testGaussBlur(finalBitmap,10,10), 1)); mTxt.setText(String.valueOf(1) + "%"); Log.d(TAG, "onProgressChanged: 左滑过界," + 1); } } else { // 右滑 变清晰 if ((mProgress + progress) < 100 && (mProgress + progress) > 0) { mImageView.setImageBitmap(SketchUtil.testGaussBlur(Utils.setAlpha(finalBitmap, (mProgress + progress)),10,10)); mTxt.setText(String.valueOf((mProgress + progress)) + "%"); Log.d(TAG, "onProgressChanged: 右滑," + (mProgress + progress)); } else if ((mProgress + progress) > 100) { mImageView.setImageBitmap(Utils.setAlpha(SketchUtil.testGaussBlur(finalBitmap,10,10), 100)); mTxt.setText(String.valueOf(100) + "%"); Log.d(TAG, "onProgressChanged: 右滑过界," + 100); } } } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { mTxt.setVisibility(View.GONE); mProgress = seekBar.getProgress(); Log.d(TAG, "onProgressChanged, onStopTrackingTouch:::" + mProgress); } }); //初始化图片 mImageView.setImageBitmap(Utils.setAlpha(finalBitmap, 100)); // 0 代表完全透明 initGesture(); } private void initGesture() { mGestureDetector = new GestureDetector(new simpleGestureListener()); // 注意以下四个声明不可缺少 mGestureImageView.setOnTouchListener(this); mGestureImageView.setFocusable(true); mGestureImageView.setClickable(true); mGestureImageView.setLongClickable(true); } @Override public boolean onTouch(View v, MotionEvent event) { // TODO Auto-generated method stub return mGestureDetector.onTouchEvent(event); } private int direction = 0; private class simpleGestureListener extends GestureDetector.SimpleOnGestureListener { /*****OnGestureListener的函数*****/ final int FLING_MIN_DISTANCE = 10; final float MIN_DISTANCE = 0, MAX_DISTANCE = 100; public boolean onDown(MotionEvent e) { // do nothing return false; } public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) { if (e1.getX() - e2.getX() > FLING_MIN_DISTANCE) { // Fling left 左滑变透明 distanceX为正值 int progress = (int) (maxDistance - distanceX / 15); if (progress >= MIN_DISTANCE) { mGestureImageView.setImageBitmap(Utils.setAlpha(finalBitmap, progress)); maxDistance = progress; mProgress = progress; mGesTxt.setVisibility(View.VISIBLE); mGesTxt.setText(String.valueOf(progress) + "%"); Log.d("MyGesture22", "onScroll:" + progress + ", 左滑 ," + distanceX); } } else if (e2.getX() - e1.getX() > FLING_MIN_DISTANCE) { // Fling right 右滑显示 distanceX为负值 int progress = (int) (maxDistance - distanceX / 5); if (progress <= MAX_DISTANCE) { mGestureImageView.setImageBitmap(Utils.setAlpha(finalBitmap, progress)); maxDistance = progress; mProgress = progress; mGesTxt.setVisibility(View.VISIBLE); mGesTxt.setText(String.valueOf(progress) + "%"); Log.d("MyGesture22", "onScroll:" + progress + ", 右滑 ," + distanceX); } } return true; } // 用户按下触摸屏、快速移动后松开,由1个MotionEvent ACTION_DOWN, 多个ACTION_MOVE, 1个ACTION_UP触发 public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) { mGesTxt.setVisibility(View.GONE); mImageView.setImageBitmap(Utils.xFerMode(bitmap,Utils.setAlpha(finalBitmap, mProgress))); Log.d(TAG, "onFling: " + mProgress); return true; } }}
可以看到,笔者先是尝试使用SeekBar的方式来实现类似的效果,即将该SeekBar置于图片的上层,然后重写其两个属性,将其设为完全透明,即对于用户不可见,同时将SeekBar设为大小同ImageView一样大,也就实现了对于图片的覆盖,然后在对SeekBar的滑动监听事件onProgressChanged()中调节效果图的透明度,同时在另一监听方法onStopTrackingTouch()记录手指上次离开屏幕时的数值,这样在下次手指down下的时候,传入该值即可。用SeekBar实现这一需求只能做到一半,如上面代码中的注视一般,可以做到记录上次滑动的数值,但是没有办法区分开手指滑动的方向,即无法验证手指左滑还是右滑。
而后,笔者采用GestureDetector的方式实现,拦截下屏幕上的触摸事件,并把事件设置给展示效果图的ImageView。这样手指滑动的事件就传递给了ImageView。然后在重写onScroll()方法,区分开手指左右滑动,并在对应的实现中对图片透明度做不同的调整;最后重写onFling()方法,识别手指抬起,将展示图片透明度数值的TextView设为不可见。
效果如下:
以下是文中使用到的工具类:用以加载大图、调节图片透明度。还有其他一些关于Bitmap的处理方法。
public class Utils { public static final String TAG = "Utils:"; private static final String SAVE_FOLDER = "PencilCamera"; private static final String SAVE_FILENAME_PREFIX = "IMG"; private static int lastSaveFileIndex = 0; /** * 图片透明度处理 * * @param sourceImg 原始图片 * @param number 透明度 * @return */ public static Bitmap setAlpha(Bitmap sourceImg, int number) { try { int[] argb = new int[sourceImg.getWidth() * sourceImg.getHeight()]; sourceImg.getPixels(argb, 0, sourceImg.getWidth(), 0, 0, sourceImg.getWidth(), sourceImg.getHeight());// 获得图片的ARGB值 number = number * 255 / 100; for (int i = 0; i < argb.length; i++) { if ((argb[i] & 0xff000000) != 0x00000000) {// 透明色不做处理 argb[i] = (number << 24) | (argb[i] & 0xFFFFFF);// 修改最高2位的值 } } sourceImg = Bitmap.createBitmap(argb, sourceImg.getWidth(), sourceImg.getHeight(), Bitmap.Config.ARGB_8888); } catch (OutOfMemoryError e) { e.printStackTrace(); System.gc(); } return sourceImg; } /** * 混合两张Bitmap 返回融合后的Bitmap * * @param src 主图 * @param dst 修饰图 */ public static Bitmap xFerMode(Bitmap src, Bitmap dst) { Bitmap lightenModeBitmap = Bitmap.createBitmap(dst.getWidth(), dst.getHeight(), Bitmap.Config.ARGB_8888); Canvas canvas = new Canvas(lightenModeBitmap); Paint paint1 = new Paint(); paint1.setAntiAlias(true); Rect srcRect = new Rect(0, 0, src.getWidth(), src.getHeight()); canvas.drawARGB(0, 0, 0, 0); canvas.drawBitmap(src, srcRect, srcRect, paint1); //画人物 src paint1.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.LIGHTEN)); // 再把原来的bitmap画到现在的bitmap canvas.drawBitmap(dst, srcRect, srcRect, paint1); //画特效 dst return lightenModeBitmap; } /** * Return the compressed bitmap using sample size. * * @param src The source of bitmap. * @param maxWidth The maximum width. * @param maxHeight The maximum height. * @param recycle True to recycle the source of bitmap, false otherwise. * @return the compressed bitmap */ public static Bitmap compressBySampleSize(final Bitmap src, final int maxWidth, final int maxHeight, final boolean recycle) { if (isEmptyBitmap(src)) return null; BitmapFactory.Options options = new BitmapFactory.Options(); options.inJustDecodeBounds = true; ByteArrayOutputStream baos = new ByteArrayOutputStream(); src.compress(Bitmap.CompressFormat.JPEG, 100, baos); byte[] bytes = baos.toByteArray(); BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); options.inSampleSize = calculateInSampleSize(options, maxWidth, maxHeight); options.inJustDecodeBounds = false; if (recycle && !src.isRecycled()) src.recycle(); return BitmapFactory.decodeByteArray(bytes, 0, bytes.length, options); } /** * Return the sample size. * 1500*1500的大图采样率为8 尺寸为562*562 * 300*400的采样率为2 * @param options The options. * @param maxWidth The maximum width. * @param maxHeight The maximum height. * @return the sample size */ private static int calculateInSampleSize(final BitmapFactory.Options options, final int maxWidth, final int maxHeight) { int height = options.outHeight; int width = options.outWidth; int inSampleSize = 1; while (height > maxHeight || width > maxWidth) { height >>= 1; width >>= 1; inSampleSize <<= 1; } return inSampleSize; } private static boolean isEmptyBitmap(final Bitmap src) { return src == null || src.getWidth() == 0 || src.getHeight() == 0; } /** * Reize bitmap with dimensions equal to or less than given params, without changing the aspect ratio * @param bitmap Input bitmap * @param maxWidth Max allowed width of resized Bitmap * @param maxHeight Max allowed height of resized Bitmap * @return Resized bitmap */ public static Bitmap resizeBitmap(Bitmap bitmap, int maxWidth, int maxHeight) { if (maxHeight > 0 && maxWidth > 0) { int width = bitmap.getWidth(); int height = bitmap.getHeight(); float ratioBitmap = (float) width / (float) height; float ratioMax = (float) maxWidth / (float) maxHeight; int finalWidth = maxWidth; int finalHeight = maxHeight; if (ratioMax > ratioBitmap) { finalWidth = (int) ((float)maxHeight * ratioBitmap); } else { finalHeight = (int) ((float)maxWidth / ratioBitmap); } Bitmap scaledBitmap = Bitmap.createScaledBitmap(bitmap, finalWidth, finalHeight, true); return scaledBitmap; } else { return bitmap; } } /** * Save bitmap as JPEG with a incremental filename, in the SAVE_FOLDER directory * @param activity * @param bitmap * @return * @throws IOException */ public static String saveBitmap(Activity activity, Bitmap bitmap) throws IOException { File file; try { // Create a new save file file = createSaveFile(); OutputStream fOut = new FileOutputStream(file); // saving the Bitmap to a file compressed as a JPEG with 85% compression rate bitmap.compress(Bitmap.CompressFormat.JPEG, 85, fOut); fOut.close(); // Add saved file to gallery Intent mediaScanIntent = new Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE); Uri contentUri = Uri.fromFile(file); mediaScanIntent.setData(contentUri); activity.sendBroadcast(mediaScanIntent); } catch(IOException e) { String errorMsg = "Unable to save image"; Log.e(TAG, errorMsg + ":" + e.getMessage()); throw new IOException(errorMsg); } return file.getPath(); } /** * Rotate bitmap by given angle in degrees * @param bitmap * @param angle * @return */ public static Bitmap rotateBitmap(Bitmap bitmap, int angle) { Matrix matrix = new Matrix(); matrix.postRotate(angle); return Bitmap.createBitmap(bitmap, 0, 0, bitmap.getWidth(), bitmap.getHeight(), matrix, true); } /** * Create a new save file in the SAVE_FOLDER directory with name as '_<3 digit serial number>' * ( example: IMG_001) * @return * @throws IOException */ private static File createSaveFile() throws IOException { File file; String path = Environment.getExternalStorageDirectory().toString(); OutputStream fOut = null; String saveFolderPath = path + '/' + SAVE_FOLDER; int saveFileIndex = lastSaveFileIndex; do { String saveFileName = SAVE_FILENAME_PREFIX + String.format("%03d", ++saveFileIndex) + ".jpg"; String saveFilePath = saveFolderPath + '/' + saveFileName; Log.d(TAG,"Saving image to path - "+saveFilePath); file = new File(saveFilePath); // the File to save to file.getParentFile().mkdirs(); } while(file.exists()); file.createNewFile(); return file; }}
转载地址:http://xqjg.baihongyu.com/