├── .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 | 实测不会有漏网之鱼,不过会有一些表情包混进来,谁让表情包也是人脸。
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
--------------------------------------------------------------------------------