お試しOpenFace 発生したエラーの解決

OpenFaceとは

顔が写った画像or動画から、顔の座標点や視線推定,頭の姿勢推定,Action Unitの強度やバイナリなどを抽出する顔解析ツールである



簡単に顔の特徴量を抽出できるためそれなりに使われているよう。
お試しでOpenFaceを使用した際に実行の部分でエラーが発生した、、

もし上手く動作しない人がいれば参考にして欲しい。

ちなみに自分は

github.com

を参考にOpenFaceを試そうとした。

上記を使用した際に、csvが出力されないといった問題が発生した。



・OpenFaceのダウンロード

まずはOpenFaceのダウンロードを行う。 github.com

から、環境に合わせて最新版のzipファイルをダウンロードし、任意の場所で展開する。

自分は、windows10環境であり、win_x64のものをダウンロードした。

ダウンロードしたフォルダには実行可能なアプリケーションがあるが、この段階で実行してもエラーが発生する。

OpenFaceを使うにはモデルデータをダウンロードする必要があるようだ。



・モデルデータのダウンロード

モデルデータのダウンロードは、batファイルを用いるのが一番簡単なよう。

echo off
powershell -ExecutionPolicy RemoteSigned -File ./download_models.ps1

上記の内容のbatファイル(sample.bat)を、展開したフォルダ内に作成する。

そして、エクスプローラーからsample.batをダブルクリックするとコード内のものがコマンドプロンプトで実行され、モデルをダウンロードする事が出来る。

これで、OpenFaceが使えるようになる。

試しにOpenFaceOfflineというアプリケーションを動かしてみたが問題なく実行出来た。

先ほど挙げた GitHub - musan6363/GUI4OpenFace: OpenFaceをGUIで操作可能なアプリケーションを作る

も上手く動作した。

かなり簡単に使えるので、研究で表情分析をする際に使用しようと思う。

javaで2分木探索(binarySearch)

binarySearchという、collectionsライブラリにあるメソッドを使ってjavaで2分木探索を行う方法を紹介します。

listに重複があるかどうかで実装方法が変わってきます。

Listに重複がない場合

この場合はCollections.binarySearchを使えば簡単に求まります。

存在しない場合は(-(挿入場所) - 1)が返ります。


    public static void main(String args []) {

        List<Integer> list = Arrays.asList(1, 2, 3, 4, 6, 9);

        //探索したいint
        int key = 4; 

        //list上の位置。 なければ負の値が返る
        int position = Collections.binarySearch(list, key);

        //keyが4の場合は、3
        //keyが5の場合は、-5
       //5の挿入場所は4なので、(-4 - 1)が返ります
        System.out.println(position);  
    }


Listに重複するものがある場合(なくても良い)

この場合はCollections.binarySearchの引数の3番目、Comparatorを使います。


    public static void main(String args[]) {

        List<Integer> list = Arrays.asList(1, 2, 2, 3, 4, 4, 4, 6, 9);

        int key = 4;

        int check = Collections.binarySearch(list, key);  //(1)

        if (check < 0) {
            System.out.println("not exist");
        } else {
            //lower_bound
            int lowerPosition = ~Collections.binarySearch(list, key, (x, y) -> x.compareTo(y) >= 0 ? 1 : -1); //(2)
            System.out.println(lowerResult);// => 4
            //upper_bound
            int upperPosition = ~Collections.binarySearch(list, key, (x, y) -> x.compareTo(y) > 0 ? 1 : -1);  //(3)
            System.out.println(upperResult);// => 7
        }

    }


(1) まずは、探索したい数がlistに存在するか確かめます。
checkが負ならば、存在しないということになります。

(2)key以上の数が始めて現れる位置を返します。
負で返されるので、反転の~を先頭に付けています。

(3)keyより大きい数が初めて現れる位置を返します。

重複して存在する場合は 、lowerPositionからupperPosition-1までkeyが存在することになります。

keyが4の場合は4から6の位置まで存在することになります。(最初の位置はもちろん0です)

もし重複していない場合はlowerPosition = upperPosition -1 になります。


以上がライブラリを使ったjavaで二分木探索をする方法です。

JavaでRestTemplateを使ってAPI通信をする方法

javaのspringで、resttemplateを使ってRest通信を行う方法を紹介します。

RestTemplateは、REST API(Web API)を呼び出すためのメソッドを提供するクラスで、Spring Frameworkが提供するHTTPクライアントです。

DTOからJson形式のリクエストに変換する処理や、Json形式のレスポンスをDTOにバインドする処理をしてくれ、 データ形式Json以外にも様々対応していています。

ここではPOSTやGETメソッドで、json形式のデータを受け取る方法を紹介します。

Serviceクラス


public class UserManager {
    @Value("${com.aaa.usercenter.url}")
    private String userCenterUrl;

    @Resource  //(1)
    private RestTemplate restTemplate;

    private static Logger logger = LoggerFactory.getLogger(UserManager.class);

    public Boolean saveUser(@RequestBody CreateReq req)  {  //(2)
        User user = new User();
        user.setAccount(req.getAccount());
        user.setPassword(req.getPassword());
        user.setName(req.getName());
        try {
            //APIに送るリクエストを作る
            RequestEntity<User> request = RequestEntity
                    .post(new URI( userCenterUrl + "/register"))   //通信先のurl
                    .accept(MediaType.APPLICATION_JSON)
                    .body(user);
            //APIからレスポンスが返ってくる
            ResponseEntity<CreateResp> response = restTemplate
                    .exchange(request, CreateResp.class);
            return Objects.requireNonNull(response.getBody()).getCheck();
        } catch (URISyntaxException e) {
            logger.error(e.toString());
            return false;
        }
    }

    public User getUserByAccount(String account)  {  //(3)
        try {
            String url = userCenterUrl + "/info";
            //request entity is created with request headers
            HttpEntity requestEntity = new HttpEntity<>(new HttpHeaders());
            UriComponentsBuilder uriBuilder = UriComponentsBuilder.fromHttpUrl(url)
                    .queryParam("account", account);
            ResponseEntity<User> response = restTemplate.exchange(
                    uriBuilder.toUriString(),
                    HttpMethod.GET,
                    requestEntity,
                    User.class
            );
            return response.getBody();
        } catch (Exception e) {
            logger.error(e.toString());
            return null;
        }
    }


(1) RestTemplateをインジェクションしています。

(2)POSTでjson形式のデータを送り、json形式のデータが返ってくるメソッドです。ここではUserクラスのデータを送っています。

(3)GETで、paramを送り、json形式のデータを要求するメソッドです。ここではUserクラスのデータが返ってきます。


json形式のbodyを送るのか、paramで通信するのか等で若干違いがあります。
用件に合わせて色々とカスタマイズできるようです。


springの日本語版ドキュメントに他のメソッドも含め色々と書いてあるので参考にしてみてください。

spring.pleiades.io

java springでCorsFilterを使ったCORS設定

java springBootで、CORSの設定方法を紹介します。

javaでは主に3つほどやり方があります。

  • @CrossOriginを使ってメソッドごとに設定する方法

  • WebMvcConfigurerAdapterを継承したconfigクラスで設定する方法

  • CorsFilterを用いて、設定する方法

があります。

ここでは、三つ目のCorsFilterを用いたCORSを有効にする設定を紹介します。


Configクラス
@Configuration
public class CorsConfig {
    @Bean
    public CorsFilter corsFilter() {
       
        CorsConfiguration config = new CorsConfiguration();
        //CORSリクエストを許可するドメイン
        config.setAllowedOrigins(new ArrayList<>(Arrays.asList(
                "https://aaa.com",
                "https://www.aaa.com",
                "http://localhost:9000",
                "http://aaa-dev.s3-website-ap-northeast-1.amazonaws.com"
                )));
        //クッキー情報を送信するかどうか
        config.setAllowCredentials(true);
        //CORSリクエストを許可するHTTPメソッド
        config.addAllowedMethod("*");
        //CORSリクエストで受信を許可するヘッダー情報
        config.addAllowedHeader("*");


        //CORSリクエストを許可するURLの形式(特に決まりがなければ「/**」でもOK)
        UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
        configSource.registerCorsConfiguration("/**", config);

        return new CorsFilter(configSource);
    }
}

CORSリクエストを許可するURLは、/aaa/**、といったようにあるパス以下全てを許可することも出来ます。


他の方法も含めて、springのドキュメントにあるのでぜひ参考にしてみてください。

CORS support in Spring Framework

JavaでS3に読み書きする方法

javaのspring bootでs3にファイルをアップロード、ダウンロードする方法を紹介します。
書き出したい文字列をファイルにアップロードするのと、s3にあるファイルを文字列として読み込む方法です。
ローカルにあるファイルをアップロードしたり、ファイル形式でダウンロードするわけではないので、注意してください。


Mavenの設定

sdk全てをダウンロードすると時間がかかるので、s3のみを使用したい場合はsdk-s3としましょう。

<!-- https://mvnrepository.com/artifact/com.amazonaws/aws-java-sdk-s3 -->
<dependency>
    <groupId>com.amazonaws</groupId
    <artifactId>aws-java-sdk-s3</artifactId
    <version>1.11.327</version>
</dependency>


s3ClientConfigクラス

s3Clientを使うためには、regionやbucketの名前などの設定が必要で、それをするためのクラスです。

@Configuration
public class S3ClientConfig {

    @Value("${cloud.aws.region.static}")
    private String s3ClientRegion;

    @Value("${aws.bucketName}")
    private String s3BucketName;

    @Value("${cloud.aws.credentials.access-key}")
    private String accessKey;

    @Value("${cloud.aws.credentials.secret-key}")
    private String secretKey;

    private AWSCredentials credentials;


    @PostConstruct
    public void init() {
        credentials = new BasicAWSCredentials(accessKey, secretKey);
    }

    @Bean
    AmazonS3 s3Client() {
        return  AmazonS3ClientBuilder
                .standard()
                .withRegion(s3ClientRegion)
                .withCredentials(new AWSStaticCredentialsProvider(credentials))
                .build();
    }
}

@Valueで、application.propertiesファイルから必要な情報を持ってきています。


読み書きを実装するクラス
@Service
public class S3Manager {

    @Value("${aws.bucketName}")
    private String s3BucketName;

    @Resource
    private AmazonS3 s3Client;

    private static Logger logger = LoggerFactory.getLogger(S3Manager.class);


    public String createFile(String objectKey, String string) {   //(1)

        s3Client.putObject(s3BucketName, objectKey, string);

        return objectKey;

    }

    public String createCsvFile(String objectKey, List<String> lines) {  //(2)
        StringBuilder sb = new StringBuilder();
        for (String line : lines) {
            sb.append(line);
            sb.append(",");
            sb.append("\n");
        }

        s3Client.putObject(s3BucketName, objectKey, sb.toString());

        return objectKey;
    }

    public String readFile(String objectKey)  {  //(3)

        if (!s3Client.doesObjectExist(s3BucketName, objectKey)) {  //(4)
            return null;
        }
        try {
            //s3objectを取得
            S3Object o = s3Client.getObject(s3BucketName, objectKey);
            S3ObjectInputStream s3is = o.getObjectContent();     //(5)
            BufferedReader br = new BufferedReader(new InputStreamReader(s3is));
            // 読み込んだ文字列を保持するストリングバッファを用意します。
            StringBuilder sb = new StringBuilder();
            // ファイルから読み込んだ一文字を保存する変数です。
            int c;
            // ファイルから1文字ずつ読み込み、バッファへ追加します。
            while ((c = br.read()) != -1) {
                sb.append((char) c);
            }
            // バッファの内容を文字列化して返します。
            br.close();
            return sb.toString();

        } catch (IOException e) {
            logger.error("error while reading file", e);
            return null;
        }

}


(1) createFileは、s3内に新しくファイルをアップロードするメソッドで引数の文字列(string)がそのままファイルになります。
objectKeyは、ファイルのkeyとなるもので、s3Client.putObjectでs3にファイルを保存する際に必要です。

(2) どんな文字列もアップロードできます。ここではcsv形式のファイルをアップロードしています。

(3) readFileメソッドは指定のobjectKeyのファイルを文字列で読み込むメソッドです。

(4)指定のkeyがこのbucketに存在するか判定しています。

(5) ファイルの中身をstream形式で読み込み、最終的に文字列にしています。


以上がs3にファイルを文字列で読み書きする方法です。
一度読み込み、文字列を追加したり消去して再度アップロードすれば簡単にs3内のファイルも編集できます。

使い方など詳しく書かれた公式のドキュメントがあるのでそれも参考にしてみてください。

https://docs.aws.amazon.com/ja_jp/sdk-for-java/v2/developer-guide/aws-sdk-java-dg.pdf

Java(Spring)でクライアントのIPアドレスを取得する方法

javaでクライアントのIPアドレスを取得する方法を紹介します。

クライアントで、サーバーの方ではないので注意してください。

サーバーにリクエストを送ったユーザーのIPアドレスを取得する方法です。

プロキシサーバーやELBを用いてる場合は、そうでない場合と違うので注意が必要です。


コード

    //IPアドレス取得
    private String getRemoteAddr(HttpServletRequest request) {
        String xForwardedFor =  request.getHeader("X-Forwarded-For");   //(1)
        //ELB等を経由していたらxForwardedForを返す
        if (xForwardedFor != null) {
            return xForwardedFor;
        }
        return request.getRemoteAddr();      //(2)
    }


HttpServletRequestを用います。

(1) ユーザーがプロキシサーバーを介してWebサーバーにアクセスしている場合や、アプリケーションがELB等のロードバランサーの背後にある場合

この場合は X-Forwarded-For httpヘッダーにアクセスして、ユーザーのIPアドレスを取得する必要があります。
request.getHeader("X-Forwarded-For")で取得することができます。
X-Forwarded-Forについては以下を参考にしてください。

ja.wikipedia.org


(2) (1)でない場合(プロキシサーバーなどを経由していない場合)

request.getRemoteAddr()だけで取得することができます。

JavaでRate Limitをする方法(bucket4j)

Rate Limitとは、一定時間あたりにアクセスできる回数(クライアントがリクエストする回数)に制限をかけることです。

過剰にアクセスしようとする悪意のあるユーザーに制限をかけることで、DOS対策になります。

ここでは、javaのspring bootを使っている時にrate limitを実装する方法を紹介します。  

色々ライブラリーがあるようですが、ここではbucket4jというライブラリーを使いました。

github.com


ゲストとloginしているユーザー、またuserのroleに応じて制限する回数を変える仕様にしました。
そして、クライアントごとに制限をかけました。


Mavenの設定
<dependency>
        <groupId>com.giffing.bucket4j.spring.boot.starter</groupId>
        <artifactId>bucket4j-spring-boot-starter</artifactId>
        <version>0.2.0</version>
</dependency>


Interceptorクラス


インターセプタとは、メソッドの前後で任意の処理を実行させることができる機能のことで、HandlerInterceptorクラスを実装したクラスはこの機能を持ちます。
リクエストがある度に、メソッドの実行前後でログを取りたい時などにも使われるようです。
ここでは、リクエストがある度にrate Limitをしたいので、このクラスを用います。

preHandleメソッドは、リクエストが実行される前に行われる処理で、falseを返すとリクエストが実行できない(制限される)ようになります。


public class PerClientRateLimitInterceptor implements HandlerInterceptor {

    private Integer anonLimit = 10;   

    private Integer commonLimit = 40;

    private Integer vipLimit = 100;

    private final Map<String, Bucket> buckets = new ConcurrentHashMap<>();   //(1)

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        String path = request.getServletPath();
  
   String remoteIp = getRemoteAddr(request);

        //loginしているユーザーの情報を取得。loginしていなければnull
        User currentUser = ShiroUtils.getUserDO();

        Bucket requestBucket;

        if (currentUser != null) {
            if (ShiroUtils.hasRole("vip")) {
                requestBucket = this.buckets.computeIfAbsent(currentUser.getAccount(), key -> vipBucket());  //(2)
            } else {
                requestBucket = this.buckets.computeIfAbsent(currentUser.getAccount(), key -> commonBucket());
            }
        } else {
            //ログインしていないユーザーはIPアドレス毎に制限
            requestBucket = this.buckets.computeIfAbsent(remoteIp, key -> anonBucket());
        }

        ConsumptionProbe probe = requestBucket.tryConsumeAndReturnRemaining(1); //(3)
        if (probe.isConsumed()) {       //(4)
            response.addHeader("X-Rate-Limit-Remaining",
                    Long.toString(probe.getRemainingTokens()));
            return true;
        }

        // 429
        response.setStatus(HttpStatus.TOO_MANY_REQUESTS.value());   //(5)
        response.addHeader("X-Rate-Limit-Retry-After-Milliseconds",
                Long.toString(TimeUnit.NANOSECONDS.toMillis(probe.getNanosToWaitForRefill())));
        return false;
    }

    //anon用
    private Bucket anonBucket() {
        return Bucket4j.builder()
                .addLimit(Bandwidth.classic(anonLimit, Refill.intervally(anonLimit, Duration.ofMinutes(1))))     //(6)
                .build();
    }

    //common用
     private Bucket commonBucket() {
        return Bucket4j.builder()
                .addLimit(Bandwidth.classic(commonLimit, Refill.intervally(commonLimit, Duration.ofMinutes(1))))
                .build();
    }

    //vip用
     private Bucket vipBucket() {
        return Bucket4j.builder()
                .addLimit(Bandwidth.classic(vipLimit, Refill.intervally(vipLimit, Duration.ofMinutes(1))))
                .build();
    }

    //IPアドレス取得    
    private String getRemoteAddr(HttpServletRequest request) {     //(7)
        String xForwardedFor =  request.getHeader("X-Forwarded-For");
        //ELB等を経由していたらxForwardedForを返す
        if (xForwardedFor != null) {
            return xForwardedFor;
        }
        return request.getRemoteAddr();
    }

}


loginしているユーザーはaccount名ごとに、loginしていない場合はIPアドレスごとに制限をかけました。
また、ユーザーのroleに応じて制限する回数を変えました。loginしていない場合のroleはanonです。


(1)識別するクライアントがkey、制限する回数を数えるbucketvalueです。

(2)ここではvipユーザーなのでvipBucketが使われています。

(3)bucketのカウントが1増えます。

(4)bucketのカウントが制限以内であればpreHandleメソッドはtrueを返します。

(5)probe.isConsumed()がfalseであれば、制限を超えたことになるので、responseのstatusを429に設定しpreHandleメソッドはfalseを返します。

(6)1分間でanonLimit回の制限をかけるbucketです。1分が経過するとbucket内のカウントがゼロになり、anonLimitを超えるとリクエストの制限が発動します。もちろん、1分間の部分や回数は自由に変えられます。

(7)クライアントのIPアドレスの取得方法です。詳しくはこちらで。

yu-memorandum.hatenablog.com


Configクラス

このクラスを作らないと、rate limitは機能しないので注意。

@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Bean
    public PerClientRateLimitInterceptor perClientRateLimitInterceptor() {
        return new PerClientRateLimitInterceptor();
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(perClientRateLimitInterceptor()).addPathPatterns("/**"); 

    }

}


addInterceptorsメソッドで、用いるInterceptorクラスを指定しています。
また、addPathPatternsの部分でInterceptを適応するパスを指定できます。
ここでは全てのパスで適応する仕様。    

これでクライアントごとにRate Limitをかけられるはずです。
ここではクライアント毎に、そしてroleに応じて制限する回数を変えたりと複雑になりましたが、色々と要望に応じてRate Limitを実装出来るようです。


実装するに当たって、この記事を参考にしました。
実装方法だけでなく、アルゴリズムも書いてあります。(英語ですが、google翻訳を使えば問題なく読めます、、、)

golb.hplar.ch