feat: add image enhancer to handle Low Dynamic Range (LDR) and Local Contrast Imbalance
This commit is contained in:
parent
32d8f2e5fb
commit
8a37ad4428
@ -0,0 +1,311 @@
|
|||||||
|
package com.zhangy.skyeye.sar.image.enhancer;
|
||||||
|
|
||||||
|
import org.opencv.core.*;
|
||||||
|
import org.opencv.imgcodecs.Imgcodecs;
|
||||||
|
import org.opencv.imgproc.Imgproc;
|
||||||
|
import org.opencv.imgproc.CLAHE;
|
||||||
|
import org.opencv.photo.Photo;
|
||||||
|
import org.opencv.photo.TonemapReinhard;
|
||||||
|
|
||||||
|
import java.nio.ShortBuffer;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ImageEnhancer.java
|
||||||
|
* ==================
|
||||||
|
* 两步组合图像亮度增强脚本(自适应参数 + 多格式兼容 Java 版)
|
||||||
|
*
|
||||||
|
* 支持的输入格式:
|
||||||
|
* - JPG : 8 bit BGR,直接兼容
|
||||||
|
* - PNG : 8/16 bit,RGB/RGBA,自动处理透明通道与位深
|
||||||
|
* - TIF : 8/16/32 bit,灰度/彩色/含透明通道,自动处理所有组合
|
||||||
|
*
|
||||||
|
* 增强步骤:
|
||||||
|
* 1. Reinhard 局部色调映射 —— 压缩动态范围,统一全局亮度
|
||||||
|
* 2. CLAHE 局部对比度增强 —— 在 LAB 空间对 L 通道进行局部直方图均衡化
|
||||||
|
*
|
||||||
|
* 所有关键参数均根据输入图像的统计特征自动计算,无需手动调参。
|
||||||
|
*
|
||||||
|
* 依赖:
|
||||||
|
* - OpenCV for Java (e.g., opencv-4.x.x.jar)
|
||||||
|
*
|
||||||
|
* 编译与运行:
|
||||||
|
* 1. 确保 opencv-4.x.x.jar 在 classpath 中。
|
||||||
|
* 2. 确保 OpenCV native library (e.g., opencv_java4xx.dll/so) 在 java.library.path 中。
|
||||||
|
* javac -cp .:/path/to/opencv-4.x.x.jar ImageEnhancer.java
|
||||||
|
* java -cp .:/path/to/opencv-4.x.x.jar -Djava.library.path=/path/to/native/libs ImageEnhancer <input_path> [output_dir]
|
||||||
|
*/
|
||||||
|
public class ImageEnhancer {
|
||||||
|
|
||||||
|
static {
|
||||||
|
try {
|
||||||
|
System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
|
||||||
|
} catch (UnsatisfiedLinkError e) {
|
||||||
|
System.err.println("Native code library failed to load.\n" + e);
|
||||||
|
System.exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存放自适应计算出的参数
|
||||||
|
*/
|
||||||
|
public static class AdaptiveParams {
|
||||||
|
double gamma, intensity, lightAdapt, colorAdapt = 0.0, clipLimit;
|
||||||
|
Size tileGrid;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format(
|
||||||
|
" gamma = %.3f%n" +
|
||||||
|
" intensity = %.3f%n" +
|
||||||
|
" light_adapt = %.3f%n" +
|
||||||
|
" clip_limit = %.3f%n" +
|
||||||
|
" tile_grid = %s",
|
||||||
|
gamma, intensity, lightAdapt, clipLimit, tileGrid
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 存放归一化结果和日志
|
||||||
|
*/
|
||||||
|
public static class NormalizedImage {
|
||||||
|
Mat image;
|
||||||
|
String log;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 对 uint16 图像做百分位截断线性拉伸后转为 uint8。
|
||||||
|
*/
|
||||||
|
private static Mat uint16ToUint8Stretch(Mat imgU16, double lowPct, double highPct) {
|
||||||
|
// 将所有通道像素合并后统一计算 lo/hi,避免各通道独立拉伸导致色偏
|
||||||
|
int totalPixels = (int) (imgU16.total() * imgU16.channels());
|
||||||
|
short[] pixels = new short[totalPixels];
|
||||||
|
imgU16.get(0, 0, pixels);
|
||||||
|
|
||||||
|
// 转换为 int 以避免排序时的符号问题
|
||||||
|
int[] pixelsInt = new int[totalPixels];
|
||||||
|
for (int i = 0; i < totalPixels; i++) {
|
||||||
|
pixelsInt[i] = pixels[i] & 0xFFFF;
|
||||||
|
}
|
||||||
|
Arrays.sort(pixelsInt);
|
||||||
|
|
||||||
|
// 使用全局统一的 lo/hi(所有通道共用),保持色彩比例
|
||||||
|
double lo = pixelsInt[(int) (totalPixels * lowPct / 100.0)];
|
||||||
|
double hi = pixelsInt[(int) (totalPixels * highPct / 100.0)];
|
||||||
|
|
||||||
|
// 使用 convertTo(alpha, beta) 做线性变换:dst = src * alpha + beta
|
||||||
|
// 等价于 (src - lo) * 255/(hi-lo) = src * (255/(hi-lo)) + (-lo * 255/(hi-lo))
|
||||||
|
double alpha = 255.0 / (hi - lo + 1e-6);
|
||||||
|
double beta = -lo * alpha;
|
||||||
|
Mat result = new Mat();
|
||||||
|
// convertTo 对所有通道均匀应用相同的 alpha/beta,不存在 Scalar 多通道问题
|
||||||
|
imgU16.convertTo(result, CvType.CV_8U, alpha, beta);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将任意常见格式的图像统一转换为 BGR uint8。
|
||||||
|
*/
|
||||||
|
public static NormalizedImage normalizeToBgrUint8(Mat img) {
|
||||||
|
NormalizedImage result = new NormalizedImage();
|
||||||
|
StringBuilder log = new StringBuilder();
|
||||||
|
log.append(String.format("原始格式:dtype=%s, shape=(%d, %d, %d)%n",
|
||||||
|
CvType.typeToString(img.type()), img.rows(), img.cols(), img.channels()));
|
||||||
|
|
||||||
|
Mat currentImg = img.clone();
|
||||||
|
List<String> steps = new ArrayList<>();
|
||||||
|
|
||||||
|
// 1. 位深归一化
|
||||||
|
if (img.depth() == CvType.CV_16U) {
|
||||||
|
currentImg = uint16ToUint8Stretch(img, 2.0, 98.0);
|
||||||
|
steps.add("uint16 → uint8 (百分位拉伸 2%~98%)");
|
||||||
|
} else if (img.depth() == CvType.CV_32F || img.depth() == CvType.CV_64F) {
|
||||||
|
img.convertTo(currentImg, CvType.CV_8U, 255.0);
|
||||||
|
steps.add(CvType.typeToString(img.type()) + " → uint8 (×255 缩放)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 通道数归一化
|
||||||
|
if (currentImg.channels() == 1) {
|
||||||
|
Imgproc.cvtColor(currentImg, currentImg, Imgproc.COLOR_GRAY2BGR);
|
||||||
|
steps.add("灰度(1ch) → BGR(3ch)");
|
||||||
|
} else if (currentImg.channels() == 4) {
|
||||||
|
Imgproc.cvtColor(currentImg, currentImg, Imgproc.COLOR_BGRA2BGR);
|
||||||
|
steps.add("BGRA(4ch) → BGR(3ch, 丢弃 Alpha)");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.append(" 转换步骤:").append(steps.isEmpty() ? "无需转换 (已是 BGR uint8)" : String.join(" | ", steps)).append("\n");
|
||||||
|
log.append(String.format(" 归一化后:dtype=%s, shape=(%d, %d, %d)",
|
||||||
|
CvType.typeToString(currentImg.type()), currentImg.rows(), currentImg.cols(), currentImg.channels()));
|
||||||
|
|
||||||
|
result.image = currentImg;
|
||||||
|
result.log = log.toString();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据输入图像的统计特征自适应计算增强参数。
|
||||||
|
*/
|
||||||
|
public static AdaptiveParams computeAdaptiveParams(Mat imgBgr) {
|
||||||
|
Mat gray = new Mat();
|
||||||
|
Imgproc.cvtColor(imgBgr, gray, Imgproc.COLOR_BGR2GRAY);
|
||||||
|
Mat grayF = new Mat();
|
||||||
|
gray.convertTo(grayF, CvType.CV_32F);
|
||||||
|
|
||||||
|
int H = gray.rows(), W = gray.cols(), totalPixels = H * W;
|
||||||
|
AdaptiveParams params = new AdaptiveParams();
|
||||||
|
|
||||||
|
// ── Reinhard gamma 语义说明 ────────────────────────────────────────────
|
||||||
|
// OpenCV Reinhard 中 gamma 越大 → 输出越亮(与传统 gamma 校正语义相反)
|
||||||
|
// 由实验扫描拟合(la=0.8, intensity=0):mean_out ≈ 79.2 × gamma
|
||||||
|
// target_mean 根据图像均值自适应:图越暗 → 目标越高(最大提亮到 128)
|
||||||
|
// 1. 图像均值 → gamma
|
||||||
|
double meanVal = Core.mean(grayF).val[0];
|
||||||
|
double meanNorm = meanVal / 255.0;
|
||||||
|
double targetMean = Math.min(128.0, 128.0 * Math.pow(1.0 - meanNorm, 0.3));
|
||||||
|
params.gamma = Math.max(0.8, Math.min(3.0, targetMean / 79.2));
|
||||||
|
|
||||||
|
// 2. intensity 固定为 0:亮度控制完全由 gamma 承担
|
||||||
|
// 避免 intensity 与 gamma 叠加导致方向混乱
|
||||||
|
params.intensity = 0.0;
|
||||||
|
|
||||||
|
// 3. 全局标准差 → light_adapt
|
||||||
|
// std 越小(对比度越低)→ 越需要局部自适应 → light_adapt 越大
|
||||||
|
// 上限 0.9(1.0 会产生 NaN),下限 0.5
|
||||||
|
MatOfDouble mean = new MatOfDouble(), stddev = new MatOfDouble();
|
||||||
|
Core.meanStdDev(grayF, mean, stddev);
|
||||||
|
double stdGlobal = stddev.get(0, 0)[0] / 255.0;
|
||||||
|
params.lightAdapt = Math.max(0.5, Math.min(0.9, 0.9 - stdGlobal));
|
||||||
|
|
||||||
|
// 4. 局部标准差均值 → clipLimit
|
||||||
|
int tile = 8;
|
||||||
|
List<Double> localStds = new ArrayList<>();
|
||||||
|
for (int y = 0; y < H - tile; y += tile) {
|
||||||
|
for (int x = 0; x < W - tile; x += tile) {
|
||||||
|
Mat patch = new Mat(grayF, new Rect(x, y, tile, tile));
|
||||||
|
Core.meanStdDev(patch, mean, stddev);
|
||||||
|
localStds.add(stddev.get(0, 0)[0]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
double meanLocalStd = localStds.stream().mapToDouble(d -> d).average().orElse(20.0);
|
||||||
|
double clipLimit = 2.0 * (30.0 / (meanLocalStd + 1e-6));
|
||||||
|
params.clipLimit = Math.max(1.0, Math.min(6.0, clipLimit));
|
||||||
|
|
||||||
|
// 5. 图像短边分辨率 → tileGridSize
|
||||||
|
int tileSize = Math.max(8, Math.min(H, W) / 16);
|
||||||
|
params.tileGrid = new Size(tileSize, tileSize);
|
||||||
|
|
||||||
|
gray.release(); grayF.release(); mean.release(); stddev.release();
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第一步:Reinhard 局部色调映射。
|
||||||
|
*/
|
||||||
|
public static Mat step1Reinhard(Mat imgBgr, AdaptiveParams params) {
|
||||||
|
Mat imgF32 = new Mat();
|
||||||
|
imgBgr.convertTo(imgF32, CvType.CV_32FC3, 1.0 / 255.0);
|
||||||
|
|
||||||
|
TonemapReinhard tonemap = Photo.createTonemapReinhard(
|
||||||
|
(float) params.gamma, (float) params.intensity, (float) params.lightAdapt, (float) params.colorAdapt);
|
||||||
|
Mat mapped = new Mat();
|
||||||
|
tonemap.process(imgF32, mapped);
|
||||||
|
|
||||||
|
// 将 NaN/Inf 替换为 0,再裁剪到合法范围
|
||||||
|
Core.patchNaNs(mapped, 0.0);
|
||||||
|
|
||||||
|
Mat result = new Mat();
|
||||||
|
mapped.convertTo(result, CvType.CV_8UC3, 255.0);
|
||||||
|
|
||||||
|
imgF32.release(); mapped.release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 第二步:CLAHE 局部对比度增强。
|
||||||
|
*/
|
||||||
|
public static Mat step2Clahe(Mat imgBgr, AdaptiveParams params) {
|
||||||
|
Mat lab = new Mat();
|
||||||
|
Imgproc.cvtColor(imgBgr, lab, Imgproc.COLOR_BGR2Lab);
|
||||||
|
|
||||||
|
List<Mat> labPlanes = new ArrayList<>(3);
|
||||||
|
Core.split(lab, labPlanes);
|
||||||
|
|
||||||
|
CLAHE clahe = Imgproc.createCLAHE(params.clipLimit, params.tileGrid);
|
||||||
|
Mat lEnhanced = new Mat();
|
||||||
|
clahe.apply(labPlanes.get(0), lEnhanced);
|
||||||
|
|
||||||
|
labPlanes.set(0, lEnhanced);
|
||||||
|
Mat resultLab = new Mat();
|
||||||
|
Core.merge(labPlanes, resultLab);
|
||||||
|
|
||||||
|
Mat result = new Mat();
|
||||||
|
Imgproc.cvtColor(resultLab, result, Imgproc.COLOR_Lab2BGR);
|
||||||
|
|
||||||
|
lab.release(); lEnhanced.release(); resultLab.release();
|
||||||
|
for(Mat p : labPlanes) p.release();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主流程:对输入图像执行两步增强并保存结果。
|
||||||
|
*/
|
||||||
|
public static void enhance(String inputPath, String outputDir) {
|
||||||
|
Mat imgRaw = Imgcodecs.imread(inputPath, Imgcodecs.IMREAD_UNCHANGED);
|
||||||
|
if (imgRaw.empty()) {
|
||||||
|
System.err.println("无法读取图像: " + inputPath);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String baseName = inputPath.substring(
|
||||||
|
inputPath.lastIndexOf("/") >= 0 ? inputPath.lastIndexOf("/") + 1 : 0,
|
||||||
|
inputPath.lastIndexOf(".")
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. 格式归一化
|
||||||
|
System.out.println("── 格式归一化 ──────────────────────────────");
|
||||||
|
NormalizedImage normResult = normalizeToBgrUint8(imgRaw);
|
||||||
|
Mat imgBgr = normResult.image;
|
||||||
|
System.out.println(normResult.log);
|
||||||
|
|
||||||
|
// 2. 计算自适应参数
|
||||||
|
System.out.println("── 自适应参数 ──────────────────────────────");
|
||||||
|
AdaptiveParams params = computeAdaptiveParams(imgBgr);
|
||||||
|
System.out.println(params);
|
||||||
|
System.out.println("────────────────────────────────────────────");
|
||||||
|
|
||||||
|
// 3. 第一步:Reinhard 色调映射
|
||||||
|
Mat resultStep1 = step1Reinhard(imgBgr, params);
|
||||||
|
String pathStep1 = outputDir + "/" + baseName + "_step1_tonemap.png";
|
||||||
|
Imgcodecs.imwrite(pathStep1, resultStep1);
|
||||||
|
System.out.println("第一步结果已保存: " + pathStep1);
|
||||||
|
|
||||||
|
// 4. 第二步:CLAHE 局部增强
|
||||||
|
Mat resultStep2 = step2Clahe(resultStep1, params);
|
||||||
|
String pathStep2 = outputDir + "/" + baseName + "_step2_clahe.png";
|
||||||
|
Imgcodecs.imwrite(pathStep2, resultStep2);
|
||||||
|
System.out.println("第二步结果已保存: " + pathStep2);
|
||||||
|
|
||||||
|
imgRaw.release(); imgBgr.release(); resultStep1.release(); resultStep2.release();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
if (args.length < 1) {
|
||||||
|
System.out.println("用法: java ImageEnhancer <input_path> [output_dir]");
|
||||||
|
String inputPath = "/home/ubuntu/upload/pasted_file_a5uhAu_8986130c06b1e8661387e859b5d2ac93.png";
|
||||||
|
String outputDir = "/home/ubuntu";
|
||||||
|
System.out.println("\n使用默认路径进行处理:");
|
||||||
|
enhance(inputPath, outputDir);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String inputPath = args[0];
|
||||||
|
String outputDir = (args.length > 1) ? args[1] : new java.io.File(inputPath).getParent();
|
||||||
|
if (outputDir == null) outputDir = ".";
|
||||||
|
new java.io.File(outputDir).mkdirs();
|
||||||
|
|
||||||
|
enhance(inputPath, outputDir);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user