├── .gitignore ├── LICENSE ├── QGroupFace ├── .classpath ├── .project ├── .settings │ └── org.eclipse.jdt.core.prefs ├── libs │ └── faceppsdk_min.jar └── src │ └── service │ ├── Config.java │ ├── FileThread.java │ ├── FileUtils.java │ └── Main.java ├── README.md ├── image └── sad.jpg └── release ├── QGroupFace.jar ├── doFliter.bat └── work.bat /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | QGroupFace/bin/ 3 | 4 | # Mobile Tools for Java (J2ME) 5 | .mtj.tmp/ 6 | 7 | # Package Files # 8 | *.war 9 | *.ear 10 | 11 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 12 | hs_err_pid* 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /QGroupFace/.classpath: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /QGroupFace/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | QGroupFace 4 | 5 | 6 | 7 | 8 | 9 | org.eclipse.jdt.core.javabuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.jdt.core.javanature 16 | 17 | 18 | -------------------------------------------------------------------------------- /QGroupFace/.settings/org.eclipse.jdt.core.prefs: -------------------------------------------------------------------------------- 1 | eclipse.preferences.version=1 2 | org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled 3 | org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.7 4 | org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve 5 | org.eclipse.jdt.core.compiler.compliance=1.7 6 | org.eclipse.jdt.core.compiler.debug.lineNumber=generate 7 | org.eclipse.jdt.core.compiler.debug.localVariable=generate 8 | org.eclipse.jdt.core.compiler.debug.sourceFile=generate 9 | org.eclipse.jdt.core.compiler.problem.assertIdentifier=error 10 | org.eclipse.jdt.core.compiler.problem.enumIdentifier=error 11 | org.eclipse.jdt.core.compiler.source=1.7 12 | -------------------------------------------------------------------------------- /QGroupFace/libs/faceppsdk_min.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintercoder/QGroupFace/58378ef1a677b37f4176a9ba1f313ade3dfa8921/QGroupFace/libs/faceppsdk_min.jar -------------------------------------------------------------------------------- /QGroupFace/src/service/Config.java: -------------------------------------------------------------------------------- 1 | package service; 2 | 3 | public class Config { 4 | public static String srcPath = ""; 5 | public static String destPath = ""; // 绝对路径 6 | public static boolean delSrcFile = false; //是否删除源图片 7 | public static int threadNum = 10; //太多会在网络上处理不过来 8 | 9 | public static int whiteRGBFileSizeMax = 50*1024; //50K 大于这个文件大小的就不检测白颜色了,提高速度 10 | public static int whiteRGBMin = 240; 11 | public static int whiteRGBRate = 50; 12 | 13 | public static String API_KEY = "4480afa9b8b364e30ba03819f3e9eff5"; 14 | public static String API_SECRET = "Pz9VFT8AP3g_Pz8_dz84cRY_bz8_Pz8M"; 15 | 16 | public static void initConfig(String srcPath,String destPath,boolean delSrcFile,int threadNum) { 17 | Config.srcPath = srcPath; 18 | Config.destPath = destPath; 19 | Config.delSrcFile = delSrcFile; 20 | Config.threadNum = threadNum; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /QGroupFace/src/service/FileThread.java: -------------------------------------------------------------------------------- 1 | package service; 2 | 3 | import java.io.File; 4 | import java.util.List; 5 | import java.util.concurrent.atomic.AtomicInteger; 6 | 7 | import org.json.JSONException; 8 | import org.json.JSONObject; 9 | 10 | import com.facepp.error.FaceppParseException; 11 | import com.facepp.http.HttpRequests; 12 | import com.facepp.http.PostParameters; 13 | 14 | public class FileThread implements Runnable { 15 | private List list; 16 | private AtomicInteger maleIndex = new AtomicInteger(1); 17 | private AtomicInteger femaleIndex = new AtomicInteger(1); 18 | 19 | private HttpRequests httpRequests = new HttpRequests( 20 | Config.API_KEY, Config.API_SECRET, true, true); 21 | 22 | private static String getGenderOrNull(JSONObject result) { 23 | String gender; 24 | 25 | try { 26 | gender = result.getJSONArray("face").getJSONObject(0) 27 | .getJSONObject("attribute").getJSONObject("gender") 28 | .getString("value").toString(); 29 | } catch (JSONException e) { 30 | return null; 31 | } 32 | return gender; 33 | } 34 | 35 | /** 36 | * 无锁并发:对需要处理的文件id取模分段,不同线程处理不同的文件 37 | * 如m个文件n个线程则 1号线程处理 1,1+n,1+2n...1+kn 号文件(1+kn " + e.getErrorMessage()); 54 | continue; 55 | } 56 | //检查是不是很多白色像素来去表情包 57 | if(file.length() < Config.whiteRGBFileSizeMax){ 58 | if(FileUtils.isImageWhite(file,Config.whiteRGBMin,Config.whiteRGBRate)){ 59 | continue; 60 | } 61 | } 62 | 63 | String gender = getGenderOrNull(result); 64 | if (null == gender) { 65 | }else{ 66 | int index = -9999; 67 | if("Female".equals(gender)){ 68 | index = femaleIndex.getAndIncrement(); 69 | }else{ 70 | index = maleIndex.getAndIncrement(); 71 | } 72 | 73 | //有些图片是.null后缀如 : ZJ0P65PZN$CW6IU)2PJVMDQ.null 74 | String destSuffix = FileUtils.getFileSuffix(file); 75 | if(destSuffix.equals("null")){ 76 | destSuffix = "jpg"; 77 | } 78 | FileUtils.fileChannelCopy(file, new File(Config.destPath 79 | + "\\" + gender + "\\" + index + "." + destSuffix)); 80 | 81 | if(Config.delSrcFile){ //删源文件 82 | file.delete(); 83 | } 84 | } 85 | } 86 | } 87 | 88 | public void setList(List list) { 89 | this.list = list; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /QGroupFace/src/service/FileUtils.java: -------------------------------------------------------------------------------- 1 | package service; 2 | 3 | import java.awt.image.BufferedImage; 4 | import java.io.File; 5 | import java.io.FileInputStream; 6 | import java.io.FileOutputStream; 7 | import java.io.IOException; 8 | import java.nio.channels.FileChannel; 9 | 10 | import javax.imageio.ImageIO; 11 | 12 | public class FileUtils { 13 | private static final long FILE_SIZE_MIN = 12*1024;// 12kb 14 | private static final long FILE_SIZE_MAX = 1024*1024; // 1M 15 | 16 | 17 | /** 18 | * 初步过滤不含皂片的文件 19 | * @param file 20 | * @return 21 | */ 22 | public static boolean canFliter(File file ) { 23 | long length = file.length(); 24 | if (length < FILE_SIZE_MIN || length > FILE_SIZE_MAX) { 25 | return true; 26 | } 27 | String Suffix = getFileSuffix(file); 28 | if("gif".equals(Suffix)) 29 | return true; 30 | return false; 31 | } 32 | 33 | /** 34 | * 检测是不是大量白色的表情包,扫一次像素统计白色像素所占比例,超过则认为是表情包 35 | * @param path 36 | * @param whiteRGB 白色的RGB值下限,参考值:>=240 && <=255 37 | * @param rateMax 白色像素个数/总像素数,参考值: >50 38 | * @return 39 | */ 40 | public static boolean isImageWhite(File file,int whiteRGB,int rateMax) { 41 | try { 42 | BufferedImage img = ImageIO.read(file); 43 | int wdith = img.getWidth(); 44 | int height = img.getHeight(); 45 | int rgb[] = new int[3]; 46 | int cnt = 0; 47 | for (int i = 0; i < wdith; i++) { 48 | for (int j = 0; j < height; j++) { 49 | int pixel = img.getRGB(i, j); 50 | rgb[0] = (pixel & 0xff0000) >> 16; 51 | rgb[1] = (pixel & 0xff00) >> 8; 52 | rgb[2] = (pixel & 0xff); 53 | if (rgb[0] >= whiteRGB && rgb[1] >= whiteRGB && rgb[2] >= whiteRGB) { 54 | cnt++; 55 | } 56 | } 57 | } 58 | double rate = 1.0 * cnt / (wdith * height) * 100; 59 | if(rate > rateMax){ 60 | return true; 61 | } 62 | } catch (IOException e) {System.out.println("Exception :" + file.getPath());} 63 | return false; 64 | } 65 | 66 | /** 67 | * 如果目标目录不存在就新建,包括其性别分类子目录 68 | * @param destPath 69 | */ 70 | public static void initDestFileDir(String destPath) { 71 | File rootDir = new File(destPath); 72 | if (!rootDir.exists()) { 73 | rootDir.mkdirs(); 74 | File maleDir = new File(destPath + "\\" + "Male"); 75 | File femaleDir = new File(destPath + "\\" + "Female"); 76 | if (!maleDir.exists()) 77 | maleDir.mkdir(); 78 | if (!femaleDir.exists()) 79 | femaleDir.mkdir(); 80 | } 81 | } 82 | 83 | public static String getFileSuffix(File file) { 84 | String fineName = file.getName(); 85 | return fineName.substring(fineName.lastIndexOf(".") + 1, 86 | fineName.length()); 87 | } 88 | 89 | /** 90 | * 复制文件 91 | * @param src 92 | * @param dest 93 | */ 94 | public static void fileChannelCopy(File src, File dest) { 95 | FileInputStream fi = null; 96 | FileOutputStream fo = null; 97 | FileChannel in = null, out = null; 98 | try { 99 | fi = new FileInputStream(src); 100 | fo = new FileOutputStream(dest); 101 | in = fi.getChannel(); 102 | out = fo.getChannel(); 103 | in.transferTo(0, in.size(), out); 104 | } catch (IOException e) { 105 | e.printStackTrace(); 106 | } finally { 107 | try { 108 | fi.close(); 109 | in.close(); 110 | fo.close(); 111 | out.close(); 112 | } catch (IOException e) { 113 | e.printStackTrace(); 114 | } 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /QGroupFace/src/service/Main.java: -------------------------------------------------------------------------------- 1 | package service; 2 | 3 | import java.io.File; 4 | import java.util.ArrayList; 5 | import java.util.List; 6 | 7 | public class Main { 8 | 9 | public static void main(String[] args) { 10 | if(args.length == 2){ 11 | Config.initConfig(args[0],args[1],false,Config.threadNum); 12 | }else if(args.length == 3){ 13 | Config.initConfig(args[0],args[1],args[2].toUpperCase().equals("Y"),Config.threadNum); 14 | }else if(args.length == 4){ 15 | Config.initConfig(args[0],args[1],args[2].toUpperCase().equals("Y"),Integer.parseInt(args[3])); 16 | } 17 | FileUtils.initDestFileDir(Config.destPath); 18 | 19 | try { 20 | List list = getFliteredFileList(Config.srcPath); 21 | System.out.println("After fliter files number: "+list.size()); 22 | System.out.println("Thread number: " + Config.threadNum ); 23 | System.out.println("Delete sources picture: " + Config.delSrcFile); 24 | System.out.println(); 25 | 26 | FileThread fileThread = new FileThread(); 27 | fileThread.setList(list); 28 | 29 | Thread[] threads = new Thread[Config.threadNum]; 30 | for (int i = 0; i < threads.length; i++) { 31 | threads[i] = new Thread(fileThread,""+i); 32 | threads[i].start(); 33 | } 34 | } catch (Exception e) { 35 | e.printStackTrace(); 36 | } 37 | 38 | } 39 | 40 | 41 | private static List getFliteredFileList(String srcPath) { 42 | File root = new File(srcPath); 43 | File[] files = root.listFiles(); 44 | 45 | List list = new ArrayList(); 46 | for (File file : files) { 47 | if (!file.isDirectory()) { 48 | if(FileUtils.canFliter(file)){ 49 | continue; 50 | } 51 | list.add(file); 52 | } 53 | } 54 | return list; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Q群爆照图片查找 2 | 3 | 通过 Face++ API 实现在QQ群图片记录里查找爆照图片,**包括被撤销的图片**,学校新生咨询群必备良品。 4 | 5 | 实测不会有漏网之鱼,不过会有一些表情包混进来,谁让表情包也是人脸。![](image/sad.jpg) 6 | 7 | ## 实现 8 | Windows版QQ在收到Q群图片后会将图片存在 `数据目录\Q号\Image\Group\` (该目录默认在*我的文档* 里),即使对方撤销也不会被删除。所以遍历该目录下的图片,初步过滤后将剩余文件交给人脸识别API,把带人脸的图片复制到新目录。 9 | 10 | ### 过滤 11 | 1. 文件大小 < 12KB 或 > 1MB 12 | 2. gif 后缀 13 | 3. < 50KB 且 含大量白色像素的表情包,默认白色像素超过50%为表情包 14 | 15 | ## 用法 16 | 打开[release下的work.bat](release/work.bat)修改其中的图片路径,保存后双击运行。 17 | 18 | 参数: 19 | 20 | java -jar QGroupFace.jar 源图片目录 输出目录 [复制后是否删源文件(Y/N)] [线程数(默认10)] 21 | 22 | 如: 23 | 24 | java -jar QGroupFace.jar E:\Software\QQ_data\10001\Image\Group\Image7 E:\test 25 | 26 | 如需将输出重定向到当前目录文本里则加上 `>> log.txt` 。 27 | 28 | 然后慢慢等待,最好去吃个饭,10个线程情况下100个文件大概1分钟,10个线程比较合适,线程过多会出现Face++的异常。 29 | 30 | ## 注意事项 31 | 1. 使用前建议先用QQ自带的消息删除器删除很久前的图片,可以的话也用[批处理文件(记得改路径)](release/doFliter.bat)过滤删除一些不太可能是皂片的文件。 32 | 2. 调用API需要 `API Key` 和 `API Secret`,代码里已使用官方的测试Key,貌似是上线版(不限制并发数),而自己注册的账号默认是并发数限制为3。 33 | 3. QQ有些图片是 `.null` 为后缀,实际上是图片,默认当作 `jpg` 处理。 34 | 4. QQ有些动图也被保存为 jpg 格式。 35 | 36 | ## License 37 | **The MIT License** -------------------------------------------------------------------------------- /image/sad.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintercoder/QGroupFace/58378ef1a677b37f4176a9ba1f313ade3dfa8921/image/sad.jpg -------------------------------------------------------------------------------- /release/QGroupFace.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintercoder/QGroupFace/58378ef1a677b37f4176a9ba1f313ade3dfa8921/release/QGroupFace.jar -------------------------------------------------------------------------------- /release/doFliter.bat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wintercoder/QGroupFace/58378ef1a677b37f4176a9ba1f313ade3dfa8921/release/doFliter.bat -------------------------------------------------------------------------------- /release/work.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | echo Begin: %time:~0,8% 3 | java -jar QGroupFace.jar E:\Software\QQ_data\792875586\Image\Group\Image7 E:\test 4 | echo End: %time:~0,8% 5 | pause --------------------------------------------------------------------------------