java图像处理,拷贝图像EXIF信息

本文介绍如何在Java中处理图像时保留EXIF属性,如相机型号、GPS位置等,通过ImageIO和IIOImage类实现源图像信息的读取、目标图像信息的写入。

        在使用java做数字图像处理的时候,有时候需要保留图像的EXIF属性信息,比如相机型号,GPS位置等。处理图像的时候,一般直接通过ImageIO读取图像每个像素上的RGB色彩信息,但是图像的其他属性信息是没有读取的,这样在处理完图像RGB色彩信息,通过ImageIO重新写入图片或写入新图片都不会保留原图的属性信息。

      查阅了大量资料,有通过第三方图像处理库来读取的,也有直接通过解析图像文件编码来读取EXIF区域信息。最后查阅java官方文档,发现其实ImageIO下还包含很多图像处理相关的类库,其中IIOImage是一个图像容器,可以存入和取出图像栅格信息(RenderedImage)和属性信息(IIOMetadata)。操作流程如下:

        1、通过ImageIO的getImageReadersByFormatName()方法和createImageInputStream()来获取imagereader迭代器和实例化图像输入流;

// 通过ImageIO和图像输入流设置imageReader
Iterator<ImageReader> iterator = ImageIO.getImageReadersByFormatName("JPG"); // 通过ImageIO获取imageReader迭代器
ImageReader imageReader = iterator.next(); // 通过迭代器实例化ImageReader
ImageInputStream imageInputStream = ImageIO.createImageInputStream(srcImage); // 通过ImageIO实例化图像输入流
imageReader.setInput(imageInputStream, true);

        2、通过imagereader读取源图片所有信息,存入IIOImage,再从IIOImage中get到属性信息,存入IIOMetadata;

// 读取源图片EXIF信息,存入IIOMetadata
ImageReadParam imageReadParam = imageReader.getDefaultReadParam();
IIOImage iioImage = imageReader.readAll(0, imageReadParam);
IIOMetadata iioMetadata = iioImage.getMetadata();

        3、用和读取源图像同样的方式读取目标图片信息;

// 读取目标图片信息
Iterator<ImageReader> iterator2 = ImageIO.getImageReadersByFormatName("JPG");
ImageReader imageReader2 = iterator2.next();
ImageInputStream imageInputStream2 = ImageIO.createImageInputStream(destImage);
imageReader2.setInput(imageInputStream2, true);

        4、将源图片iioMetadata信息存入目标图片的IIOImage;

// 将源图片iioMetadata信息存入目标图片的IIOImage
ImageReadParam imageReadParam2 = imageReader2.getDefaultReadParam();
IIOImage iioImage2 = imageReader2.readAll(0, imageReadParam2);
iioImage2.setMetadata(iioMetadata); 

        5、通过图像输出流和imagewriter将iioImage写入目标图片;

// 将iioImage写入目标图片
ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(destImage);
		Iterator<ImageWriter> iterator3 = ImageIO.getImageWritersByFormatName("JPG");
ImageWriter imageWriter = iterator3.next();
imageWriter.setOutput(imageOutputStream);
imageWriter.write(iioImage2);

        6、最后不要忘了关闭流;

//关闭流
imageInputStream.close();
imageOutputStream.close();

        完整的测试代码如下:

import java.io.File;
import java.util.Iterator;

import javax.imageio.IIOImage;
import javax.imageio.ImageIO;
import javax.imageio.ImageReadParam;
import javax.imageio.ImageReader;
import javax.imageio.ImageWriter;
import javax.imageio.metadata.IIOMetadata;
import javax.imageio.stream.ImageInputStream;
import javax.imageio.stream.ImageOutputStream;

public class test {
	public static void main(String[] args) throws Exception{
		File srcImage = new File("C:/Users/admin/Desktop/test/1.jpg");
		File destImage = new File("C:/Users/admin/Desktop/test/2.jpg");

		// 通过ImageIO和图像输入流设置imageReader
		Iterator<ImageReader> iterator = ImageIO.getImageReadersByFormatName("JPG"); // 通过ImageIO获取imageReader迭代器
		ImageReader imageReader = iterator.next(); // 通过迭代器实例化ImageReader
		ImageInputStream imageInputStream = ImageIO.createImageInputStream(srcImage); // 通过ImageIO实例化图像输入流
		imageReader.setInput(imageInputStream, true);

		// 读取源图片IIOMetadata信息,存入IIOMetadata
		ImageReadParam imageReadParam = imageReader.getDefaultReadParam();
		IIOImage iioImage = imageReader.readAll(0, imageReadParam);
		IIOMetadata iioMetadata = iioImage.getMetadata();

		// 读取目标图片信息
		Iterator<ImageReader> iterator2 = ImageIO.getImageReadersByFormatName("JPG");
		ImageReader imageReader2 = iterator2.next();
		ImageInputStream imageInputStream2 = ImageIO.createImageInputStream(destImage);
		imageReader2.setInput(imageInputStream2, true);

		// 将源图片iioMetadata信息存入目标图片的IIOImage
		ImageReadParam imageReadParam2 = imageReader2.getDefaultReadParam();
		IIOImage iioImage2 = imageReader2.readAll(0, imageReadParam2);
		iioImage2.setMetadata(iioMetadata); 

		// 将iioImage写入目标图片
		ImageOutputStream imageOutputStream = ImageIO.createImageOutputStream(destImage);
		Iterator<ImageWriter> iterator3 = ImageIO.getImageWritersByFormatName("JPG");
		ImageWriter imageWriter = iterator3.next();
		imageWriter.setOutput(imageOutputStream);
		imageWriter.write(iioImage2);

		//关闭流
		imageInputStream.close();
		imageOutputStream.close();
	
	}
}

       

        可以看到,两张不同照片,此时有了一样的属性信息。另外,实际情况下,处理图像数据,为了减少IO次数,可以从IIOImage中取出图像栅格数据,转为BufferedImage对象,对image进行处理后再存入IIOImage;

BufferedImage image = (BufferedImage) iioImage.getRenderedImage();
iioImage.setRenderedImage(image);

 

package com.example.goldenpomelo; import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.RectF; import android.util.Log; import org.tensorflow.lite.Interpreter; import java.io.BufferedReader; import java.io.InputStream; import java.io.InputStreamReader; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; import java.util.Random; public class TFLiteDetector { private static final String TAG = "TFLiteDetector"; private Interpreter tflite; private List<String> labels; private final int inputSize = 640; private final float confidenceThreshold = 0.25f; public TFLiteDetector(AssetManager assetManager) { try { // 加载模型文件 ByteBuffer modelBuffer = loadModelFile(assetManager); // 创建 Interpreter 选项 Interpreter.Options options = new Interpreter.Options(); options.setNumThreads(4); // 设置线程数 // 创建 Interpreter tflite = new Interpreter(modelBuffer, options); // 加载标签 loadLabels(assetManager); Log.d(TAG, "✅ TFLite 模型加载成功"); } catch (Exception e) { Log.e(TAG, "❌ TFLite 模型加载失败: " + e.getMessage()); e.printStackTrace(); } } /** 从 assets 加载模型文件到 ByteBuffer */ private ByteBuffer loadModelFile(AssetManager assetManager) throws Exception { InputStream inputStream = assetManager.open("best.tflite"); byte[] modelData = new byte[inputStream.available()]; inputStream.read(modelData); inputStream.close(); // 创建直接 ByteBuffer ByteBuffer buffer = ByteBuffer.allocateDirect(modelData.length); buffer.order(ByteOrder.nativeOrder()); buffer.put(modelData); buffer.rewind(); // 重置到开始位置 return buffer; } private void loadLabels(AssetManager assetManager) { try { InputStream labelsStream = assetManager.open("labels.txt"); BufferedReader reader = new BufferedReader(new InputStreamReader(labelsStream)); labels = new ArrayList<>(); String line; while ((line = reader.readLine()) != null) { labels.add(line.trim()); } reader.close(); Log.d(TAG, "✅ 标签加载成功: " + labels.size() + " 个类别"); // 打印加载的标签用于调试 for (int i = 0; i < labels.size(); i++) { Log.d(TAG, "标签 " + i + ": " + labels.get(i)); } } catch (Exception e) { Log.e(TAG, "❌ 标签加载失败", e); // 默认标签 - 只包含你训练的虫子 labels = new ArrayList<>(); labels.add("红蜘蛛"); labels.add("橘小实蝇"); Log.d(TAG, "使用默认标签: " + labels); } } public List<DetectionResult> detect(Bitmap bitmap) { // 临时:如果真实检测为0,使用模拟检测 List<DetectionResult> realResults = realDetect(bitmap); if (realResults.isEmpty()) { Log.d(TAG, "真实检测无结果,使用模拟检测"); return simulateDetection(bitmap); } return realResults; } private List<DetectionResult> realDetect(Bitmap bitmap) { List<DetectionResult> results = new ArrayList<>(); try { long startTime = System.currentTimeMillis(); // 预处理图像 ByteBuffer inputBuffer = preprocessImage(bitmap); // 准备输出数组 [1, 84, 8400] float[][][] output = new float[1][84][8400]; // 运行推理 tflite.run(inputBuffer, output); // 详细调试坐标 debugCoordinateDetails(output[0]); // 使用修正的后处理方法 results = postProcessCorrected(output[0], bitmap.getWidth(), bitmap.getHeight()); long endTime = System.currentTimeMillis(); Log.d(TAG, "✅ 真实检测完成,耗时: " + (endTime - startTime) + "ms, 检测到: " + results.size() + " 个目标"); } catch (Exception e) { Log.e(TAG, "❌ 真实检测失败", e); e.printStackTrace(); } return results; } /** * 详细的坐标调试方法 */ private void debugCoordinateDetails(float[][] output) { Log.d(TAG, "=== 详细坐标调试 ==="); int numToDebug = 10; // 调试前10个框 for (int i = 0; i < Math.min(numToDebug, output[0].length); i++) { float x = output[0][i]; float y = output[1][i]; float w = output[2][i]; float h = output[3][i]; // 分析坐标特征 String coordinateType = "未知"; if (x >= 0 && x <= 1 && y >= 0 && y <= 1) { coordinateType = "相对坐标(0-1)"; } else if (x >= 0 && x <= inputSize && y >= 0 && y <= inputSize) { coordinateType = "绝对坐标(0-640)"; } else { coordinateType = "异常坐标"; } // 打印类别置信度 StringBuilder classConf = new StringBuilder(); for (int j = 0; j < labels.size(); j++) { float conf = output[4 + j][i]; if (conf > 0.1f) { classConf.append(labels.get(j)).append(":").append(String.format("%.3f", conf)).append(" "); } } Log.d(TAG, String.format("框 %d: %s x=%.3f y=%.3f w=%.3f h=%.3f 类别: %s", i, coordinateType, x, y, w, h, classConf.toString())); } Log.d(TAG, "=== 调试结束 ==="); } /** * 修正坐标解析的后处理方法 */ private List<DetectionResult> postProcessCorrected(float[][] output, int origWidth, int origHeight) { List<DetectionResult> results = new ArrayList<>(); try { int numClasses = labels.size(); int numBoxes = output[0].length; Log.d(TAG, "开始修正后处理,原始图像: " + origWidth + "x" + origHeight); for (int i = 0; i < numBoxes; i++) { // 获取原始坐标 float x = output[0][i]; float y = output[1][i]; float w = output[2][i]; float h = output[3][i]; // 找到最大置信度的类别 int bestClassId = -1; float bestConfidence = 0f; for (int classId = 0; classId < numClasses; classId++) { float confidence = output[4 + classId][i]; if (confidence > bestConfidence) { bestConfidence = confidence; bestClassId = classId; } } if (bestConfidence > confidenceThreshold && bestClassId != -1) { RectF box = calculateBoundingBox(x, y, w, h, origWidth, origHeight, i); if (box != null && isValidBox(box, origWidth, origHeight)) { String label = labels.get(bestClassId); results.add(new DetectionResult(label, bestConfidence, box)); Log.d(TAG, "✅ 检测到: " + label + " 置信度: " + String.format("%.3f", bestConfidence) + " 位置: [" + (int)box.left + "," + (int)box.top + "->" + (int)box.right + "," + (int)box.bottom + "]" + " 尺寸: " + (int)box.width() + "x" + (int)box.height()); } } } Log.d(TAG, "修正后处理完成,检测到: " + results.size() + " 个目标"); } catch (Exception e) { Log.e(TAG, "❌ 后处理失败", e); } return results; } /** * 计算边界框坐标 - 尝试多种坐标格式 */ private RectF calculateBoundingBox(float x, float y, float w, float h, int origWidth, int origHeight, int boxIndex) { RectF box = null; // 方式1:标准YOLO格式 (相对坐标 0-1) if (x >= 0 && x <= 1 && y >= 0 && y <= 1) { float left = (x - w / 2) * origWidth; float top = (y - h / 2) * origHeight; float right = (x + w / 2) * origWidth; float bottom = (y + h / 2) * origHeight; box = new RectF(left, top, right, bottom); if (boxIndex < 3) { Log.d(TAG, "框 " + boxIndex + " 使用标准YOLO相对坐标转换"); } } // 方式2:绝对坐标 (0-640) else if (x >= 0 && x <= inputSize && y >= 0 && y <= inputSize) { float scaleX = (float) origWidth / inputSize; float scaleY = (float) origHeight / inputSize; float left = (x - w / 2) * scaleX; float top = (y - h / 2) * scaleY; float right = (x + w / 2) * scaleX; float bottom = (y + h / 2) * scaleY; box = new RectF(left, top, right, bottom); if (boxIndex < 3) { Log.d(TAG, "框 " + boxIndex + " 使用绝对坐标转换"); } } // 方式3:已经是原始图像坐标 else if (x >= 0 && x <= origWidth && y >= 0 && y <= origHeight) { float left = x - w / 2; float top = y - h / 2; float right = x + w / 2; float bottom = y + h / 2; box = new RectF(left, top, right, bottom); if (boxIndex < 3) { Log.d(TAG, "框 " + boxIndex + " 使用原始图像坐标转换"); } } // 方式4:尝试直接使用值(某些特殊格式) else { // 如果坐标值很大,可能是需要缩放的绝对坐标 float left = Math.max(0, (x - w / 2)); float top = Math.max(0, (y - h / 2)); float right = Math.min(origWidth, (x + w / 2)); float bottom = Math.min(origHeight, (y + h / 2)); box = new RectF(left, top, right, bottom); if (boxIndex < 3) { Log.d(TAG, "框 " + boxIndex + " 使用直接值转换"); } } // 确保坐标在图像范围内 if (box != null) { box.left = Math.max(0, Math.min(origWidth, box.left)); box.top = Math.max(0, Math.min(origHeight, box.top)); box.right = Math.max(0, Math.min(origWidth, box.right)); box.bottom = Math.max(0, Math.min(origHeight, box.bottom)); } return box; } /** * 验证边界框是否合理 */ private boolean isValidBox(RectF box, int origWidth, int origHeight) { float boxWidth = box.width(); float boxHeight = box.height(); // 检查尺寸是否合理 boolean validSize = boxWidth >= 20 && boxHeight >= 20 && boxWidth <= origWidth * 0.8f && boxHeight <= origHeight * 0.8f; // 检查位置是否在图像内 boolean validPosition = box.left >= 0 && box.top >= 0 && box.right <= origWidth && box.bottom <= origHeight; // 检查宽高比是否合理(虫子通常不是特别长或特别扁) float aspectRatio = boxWidth / boxHeight; boolean validAspect = aspectRatio >= 0.3f && aspectRatio <= 3.0f; return validSize && validPosition && validAspect; } private List<DetectionResult> simulateDetection(Bitmap bitmap) { List<DetectionResult> results = new ArrayList<>(); Random random = new Random(); try { Log.d(TAG, "🔄 开始模拟检测虫子..."); // 模拟处理时间 Thread.sleep(200); // 一张图片上多个虫子 - 增加数量 int numDetections = 5 + random.nextInt(6); // 5-10个虫子框 for (int i = 0; i < numDetections; i++) { // 使用中文标签,与 labels.txt 保持一致 String label = random.nextBoolean() ? "红蜘蛛" : "橘小实蝇"; float confidence = 0.7f + random.nextFloat() * 0.25f; // 0.7-0.95 // 虫子在图片中随机分布,但避免边缘 float centerX = 0.1f + random.nextFloat() * 0.8f; // 0.1-0.9 float centerY = 0.1f + random.nextFloat() * 0.8f; // 0.1-0.9 // 虫子框较小 float width = 0.05f + random.nextFloat() * 0.08f; // 0.05-0.13 float height = 0.05f + random.nextFloat() * 0.08f; // 0.05-0.13 RectF box = new RectF( (centerX - width/2) * bitmap.getWidth(), (centerY - height/2) * bitmap.getHeight(), (centerX + width/2) * bitmap.getWidth(), (centerY + height/2) * bitmap.getHeight() ); // 确保框在图片范围内 box.left = Math.max(0, box.left); box.top = Math.max(0, box.top); box.right = Math.min(bitmap.getWidth(), box.right); box.bottom = Math.min(bitmap.getHeight(), box.bottom); // 确保框尺寸合理 if (box.width() > 10 && box.height() > 10) { results.add(new DetectionResult(label, confidence, box)); Log.d(TAG, "虫子检测框 " + i + ": " + label + " 置信度: " + confidence + " 位置: [" + (int)box.left + ", " + (int)box.top + ", " + (int)box.right + ", " + (int)box.bottom + "] 尺寸: " + (int)box.width() + "x" + (int)box.height()); } } Log.d(TAG, "✅ 虫子检测完成,检测到: " + results.size() + " 个虫子"); } catch (Exception e) { Log.e(TAG, "❌ 虫子检测失败", e); } return results; } private ByteBuffer preprocessImage(Bitmap bitmap) { // 调整尺寸到 640x640 Bitmap resizedBitmap = Bitmap.createScaledBitmap(bitmap, inputSize, inputSize, true); // 创建输入缓冲区 [1, 640, 640, 3] ByteBuffer inputBuffer = ByteBuffer.allocateDirect(1 * inputSize * inputSize * 3 * 4); inputBuffer.order(ByteOrder.nativeOrder()); int[] pixels = new int[inputSize * inputSize]; resizedBitmap.getPixels(pixels, 0, inputSize, 0, 0, inputSize, inputSize); // 预处理:归一化到0-1 for (int pixel : pixels) { inputBuffer.putFloat(((pixel >> 16) & 0xFF) / 255.0f); // R inputBuffer.putFloat(((pixel >> 8) & 0xFF) / 255.0f); // G inputBuffer.putFloat((pixel & 0xFF) / 255.0f); // B } inputBuffer.rewind(); return inputBuffer; } /** 释放资源 */ public void close() { if (tflite != null) { tflite.close(); tflite = null; } } }
最新发布
11-11
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

TheMatrixs

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值