Spring boot上でJavaアプリ(ImageJ)を実行した話

### あらすじ
- 就活に向けてWebアプリのバックエンド～フロントエンドを実装してみたい
⇩
- 具体的には，AudiverisというJavaアプリをバックエンドで実行するアプリを作りたい
⇩
- 練習として，Javaアプリ(ImageJ)の利用を伴う画像ファイルのCRUDアプリを作る
⇩
- CRUDアプリまではできた <--今ここ

## 本題
この記事は，就活に使う（予定だった）webアプリの開発工程にて，特に詰まったり勉強した部分を自分用の備忘録も兼ねて記録したものになります．
今回は，バックエンドでJavaアプリを実行する方法についてです．

### 目標
ImageJを用いた処理の最終的な形としては，以下のようなものを想定しています．
1. フロントエンド側がアップロードした画像ファイルの取得
1. 画像ファイルに対する処理（今回は白黒画像への変換）
1. 変換した画像のアップロード

最終的には，1.及び3.で行うファイルのやり取りの実装も必要ですが，今回はJavaアプリの実行という目的のみに絞り，2.のみの実装を行おうと思います．
具体的には，
1. プロジェクトフォルダ内に用意した画像ファイル`test.png`を取得
1. ImageJで`test.png`を読み取り，白黒に変換
1. test.pngと同一フォルダ内に`test_edited.png`として保存

という機能の実装を目標とします．

### とりあえず実行してみた結果 -> エラーが返ってくる
ひとまず，ChatGPT等を頼りにしつつ，以下のControlerファイルとServiceファイルを作成しました．（import文は長くなるので省略しています）

```lang: ImageProcessingService.java

@Service
public class ImageProcessingService {
    private String getAbsolutePath(String relativePath){
        // ImageJは実行フォルダを認識しないので，絶対パスに直して渡す
        String executionPath = System.getProperty("user.dir");
        String absolutePath = Paths.get(executionPath, relativePath).toString();
        return absolutePath;
    }

    private String createOutputPath(String inputPath){
        // 最後のピリオド(.)位置を取得し，その前に"_edited"を挿入
        StringBuilder sb = new StringBuilder();
        sb.append(inputPath);
        int index = inputPath.lastIndexOf(".");
        sb.insert(index, "_edited");
        return sb.toString();
    }

    public String convertColorToGrey(String inputPath){
        ij.ImageJ imageJ = new ij.ImageJ();
        String outputPath = createOutputPath(inputPath);

        try {
            ImagePlus image = IJ.openImage(getAbsolutePath(inputPath));
            if(image == null){
                throw new Exception("画像が読み込めません："+inputPath);
            }

            ImageProcessor processor =  image.getProcessor();
            processor = processor.convertToByte(true);

            ImagePlus editedImage = new ImagePlus("Edited Image", processor);
            IJ.save(editedImage, getAbsolutePath(outputPath));
        } catch (Exception e) {
            System.err.println("error: "+e.getMessage());
            e.printStackTrace();
        }
        imageJ.quit();
        return outputPath;
    }
}
```

```lang:TestController.java
public class TestController {
    @Autowired
    private ImageProcessingService imageProcessingService;
    
    @GetMapping("convert")
    public void convert(){
        String tempImagePath = "//temp_images";
        String image = "/test.png";

        String imagePath = Paths.get(tempImagePath, image).toString();
        imageProcessingService.convertColorToGrey(imagePath);
    }
}
```

`TestController`に対応するhttpリクエストを送ってみると，以下のレスポンスが得られました．内容を読むと`HeadlessException`が投げられていることがわかります．

```
HTTP/1.1 500 
Vary: Origin, Access-Control-Request-Method, Access-Control-Request-Headers
Content-Type: application/json
Transfer-Encoding: chunked
Date: ***************
Connection: close

{
  "timestamp": ************,
  "status": 500,
  "error": "Internal Server Error",
  "trace": "java.awt.HeadlessException\r\n\tat ---以下省---",
  "message": "No message available",
  "path": "/test/convert"
}
```

### 原因と対策
原因は，GUI操作を伴わないことを示す`Headless`の設定が，初期設定の`true`から変更されていなかったことでした．
対策としては，アプリ実行時に設定を以下のように変更することでうまく動作しました．

```lang:BackendApplication.java
@SpringBootApplication
public class BackendApplication {

	public static void main(String[] args) {
		SpringApplication springApplication = new SpringApplication(BackendApplication.class);
		springApplication.setHeadless(false); //この行を追加
		springApplication.run(args);
	}

}
```

参考：https://qiita.com/neko_the_shadow/items/488506570e1cade4724c

### 補足：コマンドラインからの実行による対策
今回利用したImageJや，今後利用する予定のAudiverisなどのjavaアプリには，多くの場合GUIを伴わずコマンドラインから実行できる機能が備わっているようです．
将来デプロイの段階等でGUIの利用が問題になるのであれば，コマンドラインを利用した方法を利用する必要があるかもしれません（単純に処理が軽くなるメリットもありそう）．
ただ，現状ではGUIを利用する方向で問題は出ていないので，ひとまず完成に向けてGUIを利用していこうと思います．