SpringBoot+JS создает простое приложение для обнаружения целей

Spring Boot

Обзор проекта

функциональные точки

Пользователь загружает изображение, которое нужно обнаружить, во внешний интерфейс и нажимает кнопку, чтобы активировать обнаружение. Серверная часть завершает вывод алгоритма обнаружения цели для входящего изображения из внешнего интерфейса и возвращает изображение с обнаруженной ограничивающей рамкой во внешний интерфейс. Интерфейс отображает исходное изображение и результат после обнаружения.

визуализация

технический пункт

Фронтенд использует html+js+jquery, а бэкэнд использует SpringBoot+библиотеку глубокого обучения DJL, которая не задействует базу данных.

Фронтальную технологию я даже не считаю, поэтому много времени потратил на то, как передавать данные изображения во фронтенд и бэкенд, что здесь и записано.

Интерфейс загружает и передает изображения на сервер

Используйте тег input, чтобы пользователи могли загружать локальные изображения.

<input id="browse_file" type="file" accept="image/*"/>

Данные изображения инкапсулируются в форме FormData для передачи во внутреннюю службу.

var file=$('#browse_file')[0].files[0];
var formData=new FormData();    
formData.append('file', file);  //属性名称必须和后端接口@RequestParam标记的参数名一致
console.log("formData:", formData.get('file'));
$.ajax({
    url : "http://localhost:8080/detectshow/formdata",  
    type : 'POST',  
    data : formData,  
    processData : false,  //必须false才会避开jQuery对 formdata 的默认处理   
    contentType : false,  //必须false才会自动加上正确的Content-Type
    success : function(result) {  //jquery请求返回的结果好像都是字符串类型
        console.log("reponse result:", result);
    },  
    error : function(result) {  
        console.log("reponse result:", result);
        alert("Post Faile!");
    }
}); 

Внутренний интерфейс использует MultipartFile для получения данных FormData, переданных внешним интерфейсом.@RequestParam("file")Имя параметра, определяемое аннотацией, должно совпадать с именем свойства интерфейсной инкапсуляции FormData.formData.append('file', file)Сохраняйте согласованность, иначе внешний интерфейс сообщит об ошибке при вызове внутреннего сервиса:Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present] cross_road.

@ResponseBody
@RequestMapping(value="/detectshow/formdata", method = RequestMethod.POST)
public String detectShowImageOfFormData(@RequestParam("file") MultipartFile file) throws ModelException, TranslateException, IOException {
    return cvService.objectDetectShowFormData(file);
}

Серверная часть загружает изображения на внешний дисплей

Поискав в интернете, я знаю, что есть как минимум три способа:

  • Бэкэнд сохраняет изображение на сервере хранения файлов, возвращает URL-адрес изображения во внешний интерфейс, а внешний интерфейс может установить src=url тега img. Этот метод должен быть основным, но я не хочу создавать еще один сервер хранения, поэтому я не использовал этот метод.
  • Серверная часть возвращает двоичные данные изображения во внешний интерфейс в виде массива байтов, а внешний интерфейс анализирует их для отображения изображения. Однако byte[], переданный из бэкенда, полученный jquery, действительно является массивом символов, и необходимо сначала передать массив символов в массив байтов, а затем разобрать его в изображение, что не так быстро, как кодировка base64 .
  • Серверная часть возвращает кодировку изображения base64 во внешний интерфейс в виде строки, а внешний интерфейс анализирует ее для отображения изображения. Хотя существует вероятность того, что при использовании этого метода большие изображения могут быть обрезаны, выбор этого метода удобен.
$.ajax({
    url : "http://localhost:8080/detectshow/formdata",  
    type : 'POST',  
    data : formData,  
    processData : false,    
    contentType : false, 
    success : function(result) {  //jquery请求返回的结果好像都是字符串类型
        console.log("reponse result:", result);
        var src = 'data:image/png;base64,' + result;  //直接把base64编码作为img标签的src属性值
        $("#detected_img").attr('src', src);
        $("#detected_img").css("width", "70%");
        $("#detected_img").css("height", "70%");
    },  
    error : function(result) {  
        console.log("reponse result:", result);
        alert("Post Faile!");
    }
}); 

Deep Java Library

Вы можете обратиться к предыдущему блогу.

Строительство проекта

среда разработки

Win10 + JDK 8 + SpringBoot 2.4.1 + jquery 1.10.2 + DJL 0.9.0

Структура проекта

Внешний интерфейс относительно прост, всего лишь простой html-файл для загрузки локальных изображений и отправки их в фон для обработки, принятия результатов обнаружения, возвращаемых фоном, и их отображения.

Серверная часть использует SpringBoot для создания веб-приложения, предоставляет служебный интерфейс, принимает изображения, переданные внешним интерфейсом, и возвращает результирующее изображение с ограничивающей рамкой обнаружения во внешний интерфейс в виде кодировки base64. после завершения обнаружения.

  • src/pom.xmlЗависимости конфигурации файла.
  • src/main/java/[package]/SpringbootDlCvApplication.javaЗапустите портал для приложения.
  • src/main/java/[package]/controller/CVController.javaОпределите открытый Restful API.
  • src/main/java/[package]/service/CVService.javaОпределите, какие службы используются для поддержки функций API.
  • src/main/java/[package]/service/impl/CVServiceImpl.javaвыполнитьCityServiceопределенный интерфейс.
  • build/output/Сохраните изображение результата теста.

титульная страница

Внешний интерфейс загружает локальное изображение с входным тегом и отправляет изображение на серверную часть в форме FormData в качестве тела POST-запроса. При этом результат обнаружения (кодировка картинки base64), возвращаемый фоном, принимается и отображается.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Object Detection Demo</title>
    <script src="https://cdn.staticfile.org/jquery/1.10.2/jquery.min.js">
    </script>
</head>

<body>
    <h4>Please chose an image to process object detection</h4>
    <input id="browse_file" type="file" accept="image/*" onchange="showImg(this)"/>
    <button id="detect_click" onclick="detectImg()">detect</button>
    <div id="main" style="width:100%;">
        <div id="left" style="width:50%;float:left;">
            <p>origal image</p>
            <img id="origal_img" src="" alt="" width="" height="">
        </div>
        <div id="right" style="width:50%;float:left;">
            <p>detected image</p>
            <img id="detected_img" src="" alt="" width="" height="">
        </div>
    </div>

    <script>
        function detectImg() {
            console.log("object detection start...");
            var file=$('#browse_file')[0].files[0];
            var formData=new FormData();    
            formData.append('file', file);  //属性名称必须和后端接口@RequestParam标记的参数名一致
            console.log("formData:", formData.get('file'));
            $.ajax({
                url : "http://localhost:8080/detectshow/formdata",  
                type : 'POST',  
                data : formData,  
                processData : false,  //必须false才会避开jQuery对 formdata 的默认处理   
                contentType : false,  //必须false才会自动加上正确的Content-Type
                success : function(result) {  //jquery请求返回的结果好像都是字符串类型
                    console.log("reponse result:", result);
                    var src = 'data:image/png;base64,' + result;
                    $("#detected_img").attr('src', src);
                    $("#detected_img").css("width", "70%");
                    $("#detected_img").css("height", "70%");
                },  
                error : function(result) {  
                    console.log("reponse result:", result);
                    alert("Post Faile!");
                }
            }); 
            console.log("object detection end!");
        }

        function showImg(obj) {           //预览图片
            var file=$(obj)[0].files[0];  //获取文件信息
            var formData=new FormData();   
            formData.append('img',file);  //只是前端预览, 属性名称可以随便定义
            if(file) {
                var reader=new FileReader();   //调用FileReader
                reader.readAsDataURL(file);    //将文件读取为 DataURL(base64)
                reader.onload=function(evt){   //读取操作完成时触发。
                    $("#origal_img").attr('src', evt.target.result); //将img标签的src绑定为DataURL
                    $("#origal_img").css('width', "70%");
                    $("#origal_img").css('height', "70%");
                };
            }else {
                alert("上传失败");
            }
        }
    </script>
</body>
</html>

серверная служба

импортировать зависимости

В основном познакомьтесь с djl и связанными с ним зависимостями.

<!-->以BOM的方式统一管理依赖包的版本<-->
	<dependencyManagement>
		<dependencies>
			<dependency>
				<groupId>ai.djl</groupId>
				<artifactId>bom</artifactId>
				<version>0.9.0</version>
				<type>pom</type>
				<scope>import</scope>
			</dependency>
		</dependencies>
	</dependencyManagement>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

		<!-->使用djl必须引入的依赖包<-->
		<dependency>
			<groupId>ai.djl</groupId>
			<artifactId>api</artifactId>
		</dependency>
		<!-->使用不同的深度学习框架模型引入不同的依赖包, Apache MXNet engine implementation<-->
		<dependency>
			<groupId>ai.djl.mxnet</groupId>
			<artifactId>mxnet-engine</artifactId>
		</dependency>
		<!-->使用不同的深度学习框架模型引入不同的依赖包, Apache MXNet native library<-->
		<dependency>
			<groupId>ai.djl.mxnet</groupId>
			<artifactId>mxnet-native-auto</artifactId>
			<scope>runtime</scope>
		</dependency>
		<!-->使用不同的深度学习框架模型引入不同的依赖包, A ModelZoo containing models exported from Apache MXNet<-->
		<dependency>
			<groupId>ai.djl.mxnet</groupId>
			<artifactId>mxnet-model-zoo</artifactId>
		</dependency>
	</dependencies>
Определение и реализация интерфейса службы
  • src/main/java/[package]/service/CVService.javaОпределите, какие службы могут поддерживать функции API.
package com.town.djl.cv.service;

import ai.djl.ModelException;
import ai.djl.translate.TranslateException;
import com.town.djl.cv.domain.DetectResultVO;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.List;

public interface CVService {
    /*
     * 前端以FormData传入图片数据, 后台服务对该图片进行目标检测, 返回检测结果图片的base64编码
     */
    public String objectDetectShowFormData(MultipartFile file) throws IOException, ModelException, TranslateException;
}
  • src/main/java/[package]/service/CVServiceImpl.javaОпределите конкретную реализацию интерфейса службы.
//省略包名和import

@Service
public class CVServiceImpl implements CVService {
    private static final String imgRoot = "src/test/resources/";
    private static final String outRoot = "build/output";
    private static final Logger logger = LoggerFactory.getLogger(CVServiceImpl.class);

    /*对输入的Image进行目标检测,返回检测结果*/
    public DetectedObjects predict(Image img, String imageName) throws IOException, ModelException, TranslateException {
        String backbone;
        if ("TensorFlow".equals(Engine.getInstance().getEngineName())) {
            backbone = "mobilenet_v2";
        } else {
            backbone = "resnet50";
        }

        Criteria<Image, DetectedObjects> criteria =
                Criteria.builder()
                        .optApplication(Application.CV.OBJECT_DETECTION)
                        .setTypes(Image.class, DetectedObjects.class)
                        .optFilter("backbone", backbone)
                        .optProgress(new ProgressBar())
                        .build();

        try (ZooModel<Image, DetectedObjects> model = ModelZoo.loadModel(criteria)) {
            try (Predictor<Image, DetectedObjects> predictor = model.newPredictor()) {
                DetectedObjects detection = predictor.predict(img);
                saveBoundingBoxImage(img, detection, imageName);
                return detection;
            }
        }
    }

    /*将检测所得的bbox画在图上并保存的指定文件*/
    private void saveBoundingBoxImage(Image img, DetectedObjects detection, String imageName)
            throws IOException {
        Path outputDir = Paths.get(outRoot);
        Files.createDirectories(outputDir);

        // Make image copy with alpha channel because original image was jpg
        Image newImage = img.duplicate(Image.Type.TYPE_INT_ARGB);
        newImage.drawBoundingBoxes(detection);

        Path imagePath = outputDir.resolve(imageName + ".png");
        // OpenJDK can't save jpg with alpha channel
        newImage.save(Files.newOutputStream(imagePath), "png");
        logger.info("Detected objects image has been saved in: {}", imagePath);
    }

    /*DJL提供的Image格式,从InputStream读取图片*/
    public Image readImageByFormData(MultipartFile file) throws IOException {
        if (file.isEmpty()) {
            return null;
        }
        //通过输入流获取图片数据
        InputStream inStream = file.getInputStream();
        //得到图片的二进制数据,以二进制封装得到数据,具有通用性
        Image img = ImageFactory.getInstance().fromInputStream(inStream);
        //关闭输出流
        inStream.close();
        return img;
    }

    public String objectDetectShowFormData(MultipartFile file) throws IOException, ModelException, TranslateException {
        String fileName = file.getOriginalFilename().split("\\.")[0]; //获取文件名
        System.out.println(fileName);
        Image img = readImageByFormData(file);
        DetectedObjects detections = predict(img, fileName);

        File imgFile = new File(outRoot + "/" + fileName + ".png");
        InputStream inStream =new FileInputStream(imgFile);
        byte imgBytes[] = new byte[(int) imgFile.length()]; //创建合适文件大小的数组
        inStream.read(imgBytes); //读取文件里的内容到b[]数组
        inStream.close();
        BASE64Encoder encoder = new BASE64Encoder();
        return encoder.encodeBuffer(imgBytes);
    }
}
Определение интерфейса Restful API
  • src/main/java/[package]/controller/CVController.javaОпределите, какие службы могут поддерживать функции API.
//省略包名和import

@Controller
@RequestMapping("")
public class CVController {
    @Autowired
    private CVService cvService;

    @CrossOrigin  //允许跨域访问
    @ResponseBody 
    @RequestMapping(value="/detectshow/formdata", method = RequestMethod.POST)
    public String detectShowImageOfFormData(@RequestParam("file") MultipartFile file) throws ModelException, TranslateException, IOException {
        return cvService.objectDetectShowFormData(file);
    }
}

@RequestParam("file")Имя параметра, определяемое аннотацией, должно совпадать с именем свойства интерфейсной инкапсуляции FormData.formData.append('file', file)Сохраняйте согласованность, иначе внешний интерфейс сообщит об ошибке при вызове внутреннего сервиса:Resolved [org.springframework.web.multipart.support.MissingServletRequestPartException: Required request part 'file' is not present] cross_road.

Запись приложения
  • src/main/java/[package]/SpringbootDlCvApplication.javaОпределите, какие службы могут поддерживать функции API.
package com.town.djl.cv;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringbootDlCvApplication {
	public static void main(String[] args) {
		SpringApplication.run(SpringbootDlCvApplication.class, args);
	}
}

Побить пит-рекорд

Вопрос 1: Имя параметра не соответствует аннотации @RequestParam при вызове результата, и вызов сообщает об ошибке.

@RequestMapping(value="/image", method = RequestMethod.POST)
public @ResponseBody String detectImage(@RequestParam("file") MultipartFile file) {
    String fileName = file.getOriginalFilename(); //获取文件名
    System.out.println(fileName);
    return "200";
}

Решение. При вызове интерфейса параметры должны соответствовать именам параметров, указанным в аннотации @RequestParam.

Вопрос 2: Фоновая служба выполняется успешно, также есть код состояния возврата, но Postman отображает исключение.

Код:

@RequestMapping(value="/image", method = RequestMethod.POST)
public String detectImage(@RequestParam("file") MultipartFile file) {
    String fileName = file.getOriginalFilename(); //获取文件名
    System.out.println(fileName);
    return "200";
}

Результат звонка почтальона:

Решение: возвращаемый результат не определен в определении интерфейса, просто добавьте аннотацию @ResponseBody.

@RequestMapping(value="/image", method = RequestMethod.POST)
public @ResponseBody String detectImage(@RequestParam("file") MultipartFile file) {
    String fileName = file.getOriginalFilename(); //获取文件名
    System.out.println(fileName);
    return "200";
}