Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -574,6 +574,7 @@ public void onCaptureCompleted(@NonNull CameraCaptureSession session,
mColorSpaceTransform = result.get(CaptureResult.COLOR_CORRECTION_TRANSFORM);
Integer state = result.get(CaptureResult.FLASH_STATE);
mFlashed = state != null && state == CaptureResult.FLASH_STATE_PARTIAL || state == CaptureResult.FLASH_STATE_FIRED;
IsoExpoSelector.updatePreviewFlicker(result);
mPreviewCaptureResult = result;
mPreviewCaptureRequest = request;
process(result);
Expand Down Expand Up @@ -1785,6 +1786,17 @@ private void setCaptureRequestBuilder() throws CameraAccessException {
}

mPreviewRequestBuilder.addTarget(surface);
int[] antibandingModes = mCameraCharacteristics.get(
CameraCharacteristics.CONTROL_AE_AVAILABLE_ANTIBANDING_MODES);
if (antibandingModes != null) {
for (int mode : antibandingModes) {
if (mode == CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO) {
mPreviewRequestBuilder.set(CaptureRequest.CONTROL_AE_ANTIBANDING_MODE,
CaptureRequest.CONTROL_AE_ANTIBANDING_MODE_AUTO);
break;
}
}
}
synchronized (mZslBufferLock) {
while (!mZslRingBuffer.isEmpty()) {
Image img = mZslRingBuffer.pollFirst();
Expand Down Expand Up @@ -2278,6 +2290,7 @@ private void captureStillPicture() {
rebuildPreviewBuilder();*/

IsoExpoSelector.useTripod = PhotonCamera.getGyro().getTripod();
IsoExpoSelector.prepareBracketingPlan(frameCount);
if (frameCount == -1) {
for (int i = 0; i < 1; i++) {
if(!PhotonCamera.getSettings().selectedMode.equals(CameraMode.RAWVIDEO))
Expand Down Expand Up @@ -2880,4 +2893,4 @@ private void logIt() {
}

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,116 @@

public class IsoExpoSelector {
public static final int baseFrame = 1;
/**
* Capture profiles. These values are persisted, so do not reorder them.
*
* Profiles are planned for the whole burst, rather than repeated every
* three frames. Base-exposure reference frames are captured first; the one
* or two longer shadow frames are captured last.
*/
public static final int BRACKETING_OFF = 0;
public static final int BRACKETING_HDR = 1;
public static final int BRACKETING_DEEP_SHADOWS = 2;
public static final int BRACKETING_CLEAN_SHADOWS = 3;
private static final long FLICKER_SAFE_50HZ_WINDOW_NS = ExposureIndex.sec / 100;
private static final long FLICKER_SAFE_60HZ_WINDOW_NS = ExposureIndex.sec / 120;
private static final String TAG = "IsoExpoSelector";
public static boolean HDR = false;
public static boolean useTripod = false;
public static final int patternSize = 3;
public static ArrayList<ExpoPair> pairs = new ArrayList<>();
public static ArrayList<ExpoPair> fullpairs = new ArrayList<>();
public static long lastSelectedExposure = 0;
private static BracketingPlan activeBracketingPlan = BracketingPlan.constant(1);
private static volatile int detectedFlickerHz = 50;

private static final class BracketingPlan {
final float[] multipliers;
final boolean flickerSafe;

private BracketingPlan(float[] multipliers, boolean flickerSafe) {
this.multipliers = multipliers;
this.flickerSafe = flickerSafe;
}

static BracketingPlan constant(int frameCount) {
return new BracketingPlan(new float[Math.max(frameCount, 1)], false);
}

float multiplierAt(int step) {
if (step < 0 || step >= multipliers.length) return 1.f;
return multipliers[step] > 0.f ? multipliers[step] : 1.f;
}
}

/**
* Creates one exposure plan per capture. The final frame(s), not every
* third frame, receive the long exposure. This keeps a run of base RAW
* frames available as the sharp alignment reference.
*/
public static void prepareBracketingPlan(int frameCount) {
int count = Math.max(frameCount, 1);
float[] multipliers = new float[count];
int profile = HDR ? PreferenceKeys.getBracketingMode() : BRACKETING_OFF;
int shadowFrames = 0;
float shadowMultiplier = 1.f;
// Every active bracketing profile uses the preview-detected mains
// cadence. Off deliberately leaves normal single-exposure capture alone.
boolean flickerSafe = profile != BRACKETING_OFF;

switch (profile) {
case BRACKETING_HDR:
shadowFrames = 1;
shadowMultiplier = 4.f;
break;
case BRACKETING_DEEP_SHADOWS:
shadowFrames = 1;
shadowMultiplier = 8.f;
break;
case BRACKETING_CLEAN_SHADOWS:
shadowFrames = 2;
shadowMultiplier = 4.f;
break;
case BRACKETING_OFF:
default:
break;
}

// Always retain one short frame. A two-long-frame plan also needs at
// least two short frames for robust alignment and deghosting.
int minReferences = shadowFrames > 1 ? 2 : 1;
shadowFrames = Math.min(shadowFrames, Math.max(0, count - minReferences));
for (int i = count - shadowFrames; i < count; i++) {
multipliers[i] = shadowMultiplier;
}
activeBracketingPlan = new BracketingPlan(multipliers, flickerSafe);
Log.d(TAG, "Bracketing plan: frames=" + count + " profile=" + profile
+ " shadowFrames=" + shadowFrames + " multiplier=" + shadowMultiplier
+ " flickerSafe=" + flickerSafe);
}

/**
* Receives the camera HAL's scene-flicker classification from the live
* preview. Unknown and no-flicker results retain the last stable value;
* this keeps the capture cadence from changing when a momentary preview
* result omits the optional statistic.
*/
public static void updatePreviewFlicker(CaptureResult result) {
Integer flicker = result.get(CaptureResult.STATISTICS_SCENE_FLICKER);
if (flicker == null) return;
int frequency;
if (flicker == CaptureResult.STATISTICS_SCENE_FLICKER_50HZ) {
frequency = 50;
} else if (flicker == CaptureResult.STATISTICS_SCENE_FLICKER_60HZ) {
frequency = 60;
} else {
return;
}
if (detectedFlickerHz != frequency) {
detectedFlickerHz = frequency;
Log.d(TAG, "Preview flicker detected: " + detectedFlickerHz + " Hz");
}
}

// ---- Shutter-Priority / Dynamic Low-Light AE Curve ----
// Instead of letting stock 3A pick a fast shutter + high ISO, we keep the SAME
Expand Down Expand Up @@ -201,37 +304,11 @@ public static ExpoPair GenerateExpoPair(int step, CaptureController captureContr
pair.ExpoCompensateLowerExpo(2.f);
pair.ExpoCompensateLower(1.f/2.f);
}*/
if (step%patternSize == 0 && HDR) {
// Set multiplier based on bracketing mode (0=Off, 1=Normal, 2=High)
int bracketingMode = PreferenceKeys.getBracketingMode();
pair.layerMpy = 1.f;
if (bracketingMode == 1) {
// Normal bracketing (1x, 4x)
pair.layerMpy = 4.f;
} else if (bracketingMode == 2) {
// High bracketing (1x, 8x)
pair.layerMpy = 8.f;
if (HDR && step >= 0) {
applyBracketingMultiplier(pair, activeBracketingPlan.multiplierAt(step));
if (activeBracketingPlan.flickerSafe) {
applyFlickerSafeExposure(pair);
}

if (pair.layerMpy > 1.f) {
pair.curlayer = ExpoPair.exposureLayer.High;
if (pair.ExpoCompensateLowerExpo2(1.0 / pair.layerMpy)) {
pair.layerMpy = 1.f;
pair.curlayer = ExpoPair.exposureLayer.Normal;
}
} else {
pair.curlayer = ExpoPair.exposureLayer.Normal;
}
}
if ((step%patternSize == 1) && HDR) {
pair.layerMpy = 1.f;
pair.ExpoCompensateLowerExpo2(1.0 / pair.layerMpy);
pair.curlayer = ExpoPair.exposureLayer.Normal;
}
if (step%patternSize == 2 && HDR) {
pair.layerMpy = 1.f;
pair.ExpoCompensateLowerExpo2(1.0 / pair.layerMpy);
pair.curlayer = ExpoPair.exposureLayer.Normal;
}

if (pair.exposure < ExposureIndex.sec / 90 && PhotonCamera.getSettings().eisPhoto) {
Expand All @@ -249,6 +326,56 @@ public static ExpoPair GenerateExpoPair(int step, CaptureController captureContr
return pair;
}

private static void applyBracketingMultiplier(ExpoPair pair, float multiplier) {
pair.layerMpy = multiplier;
if (multiplier <= 1.f) {
pair.curlayer = ExpoPair.exposureLayer.Normal;
return;
}

pair.curlayer = ExpoPair.exposureLayer.High;
if (pair.ExpoCompensateLowerExpo2(1.0 / multiplier)) {
// A device limit prevented the requested bracket; make its metadata
// truthful so the merger never exposure-scales the frame incorrectly.
pair.layerMpy = 1.f;
pair.curlayer = ExpoPair.exposureLayer.Normal;
}
}

/**
* Keeps an HDR burst phase-neutral under the mains frequency classified by
* the preview HAL. 50 Hz lighting uses 10 ms, 20 ms, 30 ms (1/100 s,
* 1/50 s, 1/33.333 s), etc.; 60 Hz lighting uses 1/120 s windows.
* ISO is changed to keep the metered exposure energy unchanged. If no such
* shutter is possible within the sensor's shutter and ISO ranges, leave the
* metered pair untouched rather than clipping it.
*/
private static void applyFlickerSafeExposure(ExpoPair pair) {
long flickerWindowNs = detectedFlickerHz == 60
? FLICKER_SAFE_60HZ_WINDOW_NS : FLICKER_SAFE_50HZ_WINDOW_NS;
long targetEnergy = pair.exposure * (long) pair.iso;
long maxNormalizedIso = Math.round(pair.isohigh * (100.0 / pair.isolow));
long minExposure = Math.max(pair.exposurelow, ceilDivide(targetEnergy, maxNormalizedIso));
long maxExposure = Math.min(pair.exposurehigh, targetEnergy / MIN_ISO_NORMALIZED);
long minWindows = ceilDivide(minExposure, flickerWindowNs);
long maxWindows = maxExposure / flickerWindowNs;
if (minWindows > maxWindows || maxWindows < 1) {
Log.d(TAG, "Flicker-safe shutter unavailable; preserving " + pair.ExposureString());
return;
}

long requestedWindows = Math.round((double) pair.exposure / flickerWindowNs);
long windows = Math.max(minWindows, Math.min(Math.max(requestedWindows, 1), maxWindows));
pair.exposure = windows * flickerWindowNs;
pair.iso = (int) Math.round((double) targetEnergy / pair.exposure);
Log.d(TAG, "Flicker-safe " + detectedFlickerHz + " Hz exposure: "
+ pair.ExposureString() + " ISO " + pair.iso);
}

private static long ceilDivide(long dividend, long divisor) {
return dividend / divisor + (dividend % divisor == 0 ? 0 : 1);
}

public static double getMPY() {
return 100.0 / getISOLOW();
}
Expand Down Expand Up @@ -818,4 +945,4 @@ public String ExposureString() {
return ExposureIndex.sec2string(ExposureIndex.time2sec(exposure));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@

public class HdrxProcessor extends ProcessorBase {
private static final String TAG = "HdrxProcessor";
// Ignore small HAL rounding differences when assigning an exposure layer.
private static final double EXPOSURE_LAYER_TOLERANCE = 0.05;
private ArrayList<ImageFrame> mImageFramesToProcess;
private HashMap<Long, Double> exposures;
private int imageFormat;
Expand Down Expand Up @@ -125,10 +127,27 @@ private void ApplyHdrX() {
for (int i = 1; i < mImageFramesToProcess.size(); i++) {
minExpo = Math.min(minExpo, exposures.get(mImageFramesToProcess.get(i).getTimestamp()));
}
// fullpairs retains the requested bracket multiplier. Locate the 0 EV
// request and use its measured exposure energy as the semantic base.
// The merger still normalizes layerMpy against minExpo because its
// shaders require every radiometric multiplier to be >= 1.
int baseExposureIndex = 0;
float closestToBase = Float.MAX_VALUE;
int pairedFrameCount = Math.min(mImageFramesToProcess.size(), IsoExpoSelector.fullpairs.size());
for (int i = 0; i < pairedFrameCount; i++) {
float distance = Math.abs(IsoExpoSelector.fullpairs.get(i).layerMpy - 1.f);
if (distance < closestToBase) {
closestToBase = distance;
baseExposureIndex = i;
}
}
double baseExpo = exposures.get(mImageFramesToProcess.get(baseExposureIndex).getTimestamp());
Log.d(TAG, "Wrapper.init");
ArrayList<ImageFrame> images = new ArrayList<>();
int ISO = 0;
int lowFrames = 0;
int normalFrames = 0;
int shadowFrames = 0;
if(BurstShakiness.size() < mImageFramesToProcess.size()){
Log.d(TAG,"Warning: Gyro data size:"+BurstShakiness.size()+" is less than image size:"+mImageFramesToProcess.size());
}
Expand All @@ -140,9 +159,15 @@ private void ApplyHdrX() {
//frame.pair = IsoExpoSelector.pairs.get(i % IsoExpoSelector.patternSize);
frame.pair = IsoExpoSelector.fullpairs.get(i);
frame.number = i;
frame.pair.layerMpy = (float) (exposures.get(mImageFramesToProcess.get(i).getTimestamp()) / minExpo);
if (frame.pair.layerMpy > 1.0) {
double measuredExpo = exposures.get(frame.getTimestamp());
frame.pair.layerMpy = (float) (measuredExpo / minExpo);
double baseRelativeExposure = measuredExpo / baseExpo;
if (baseRelativeExposure < 1.0 - EXPOSURE_LAYER_TOLERANCE) {
frame.pair.curlayer = IsoExpoSelector.ExpoPair.exposureLayer.Low;
lowFrames++;
} else if (baseRelativeExposure > 1.0 + EXPOSURE_LAYER_TOLERANCE) {
frame.pair.curlayer = IsoExpoSelector.ExpoPair.exposureLayer.High;
shadowFrames++;
} else {
frame.pair.curlayer = IsoExpoSelector.ExpoPair.exposureLayer.Normal;
normalFrames++;
Expand All @@ -151,7 +176,8 @@ private void ApplyHdrX() {
int ind = Math.max(0,mImageFramesToProcess.size()-2);
frame.frameGyro = BurstShakiness.get(ind);
}*/
Log.d(TAG, "Mpy:" + frame.pair.layerMpy);
Log.d(TAG, "Mpy:" + frame.pair.layerMpy + " baseRelative:"
+ baseRelativeExposure + " layer:" + frame.pair.curlayer);
images.add(frame);
ISO += frame.pair.iso;
}
Expand Down Expand Up @@ -206,8 +232,22 @@ private void ApplyHdrX() {
if(normalFrames == 1 && cur.pair.curlayer == IsoExpoSelector.ExpoPair.exposureLayer.Normal) {
continue;
}
if(cur.pair.curlayer == IsoExpoSelector.ExpoPair.exposureLayer.Normal){
normalFrames--;
if(lowFrames == 1 && cur.pair.curlayer == IsoExpoSelector.ExpoPair.exposureLayer.Low) {
continue;
}
if(shadowFrames == 1 && cur.pair.curlayer == IsoExpoSelector.ExpoPair.exposureLayer.High) {
continue;
}
switch (cur.pair.curlayer) {
case Low:
lowFrames--;
break;
case Normal:
normalFrames--;
break;
case High:
shadowFrames--;
break;
}
Log.d(TAG, "Removing unlucky:" + curunlucky + " number:" + images.get(images.size() - 1).number);
images.get(images.size() - 1).close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,8 @@ private void createBracketingEntry() {
bracketingEntry.addSettingsBarButtonModels(
SettingsBarButtonModel.newButtonModel(R.id.bracketing_off_button, R.drawable.ic_exposure, R.string.bracketing_off, 0, bracketingEntry),
SettingsBarButtonModel.newButtonModel(R.id.bracketing_normal_button, R.drawable.ic_exposure, R.string.bracketing_normal, 1, bracketingEntry),
SettingsBarButtonModel.newButtonModel(R.id.bracketing_high_button, R.drawable.ic_exposure, R.string.bracketing_high, 2, bracketingEntry)
SettingsBarButtonModel.newButtonModel(R.id.bracketing_high_button, R.drawable.ic_exposure, R.string.bracketing_high, 2, bracketingEntry),
SettingsBarButtonModel.newButtonModel(R.id.bracketing_clean_shadows_button, R.drawable.ic_exposure, R.string.bracketing_clean_shadows, 3, bracketingEntry)
);
}

Expand Down Expand Up @@ -212,4 +213,4 @@ private void updateEntry(SettingsBarEntryModel entry, boolean value) {
}
}
}
}
}
3 changes: 2 additions & 1 deletion app/src/main/res/values/ids.xml
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,10 @@
<item name="bracketing_off_button" type="id"/>
<item name="bracketing_normal_button" type="id"/>
<item name="bracketing_high_button" type="id"/>
<item name="bracketing_clean_shadows_button" type="id"/>
<item name="ae_metering_std_entry_layout" type="id"/>
<item name="ae_metering_std_off_button" type="id"/>
<item name="ae_metering_std_center_button" type="id"/>
<item name="ae_metering_std_average_button" type="id"/>
<item name="ae_metering_std_spot_button" type="id"/>
</resources>
</resources>
7 changes: 4 additions & 3 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,11 @@
<string name="pref_ae_metering_std_key" translatable="false">pref_ae_metering_std_mode_key</string>

<!-- Bracketing mode strings -->
<string name="exposure_bracketing">Exposure Bracketing</string>
<string name="exposure_bracketing">Bracketing</string>
<string name="bracketing_off">Off (1x)</string>
<string name="bracketing_normal">Normal (1x, 4x)</string>
<string name="bracketing_high">High (1x, 8x)</string>
<string name="bracketing_normal">HDR (1x, 4x)</string>
<string name="bracketing_high">Deep shadows (1x, 8x)</string>
<string name="bracketing_clean_shadows">Motion-safe shadows (1x, 4x, 4x)</string>
<string name="pref_bracketing_key" translatable="false">pref_bracketing_mode_key</string>

<!-- Video Settings -->
Expand Down