Skip to main content
Andrew Bold
Known Participant
January 11, 2023
Answered

How do I automatically straighten the horizon in a photo?

  • January 11, 2023
  • 6 replies
  • 6572 views

Hello. Please help me solve the problem.

 

I have a lot of studio photos taken without a tripod, so the horizon is often tilted at different angles.
There is a clear contrast between the wall and the floor.

 

I need to automatically straighten the horizon in these photos, no change in perspective, no cropping. Just the angle of the slope.

Lightroom, camera raw are not up to the task.


The manual method, using a ruler, is not for me either.

Perhaps there is some Script that can help me solve my problem?

 

*Sample photo added.

Correct answer jazz-y

Greetings @jazz-y !
Could you help me a bit with modifying this script?
I'm not very successful in modifying it.....

I'm trying to change this script for other photos (Different background, where there is no difference in brightness between wall and floor)


But in those photos, there is a clear black bar between the wall and the floor.

I thought it would be logical to include one "Threshold"(up 12 percent) correction step in the action. To get a clear black line.

After that, to modify the script so that the script detects the black line and not the absolute difference in brightness "findFloor".

But due to my poor knowledge of scripting I was not able to get a working result.

I will be very grateful if you have an opportunity to help me


Hi! Unfortunately, such scripts are not universal and it works well only for solving a specific problem.

Try this one. It works well on the image posted to you, but there may be issues with other images.
I've modified the script so that it tries to find an area in the bottom half of the frame with a big difference in brightness, assuming that's where the floor boundary is. For large images, you can increase the size of the BATCH_SIZE constant (this is the number of consecutive pixels within which the script tries to find the maximum brightness difference)

 

var apl = new AM('application'),
    doc = new AM('document'),
    lr = new AM('layer'),
    floorY = [];
const STRIPE_SIZE = 25,
    MAX_DE = 5;
try {
    if (apl.getProperty('numberOfDocuments')) {
        var docRes = doc.getProperty('resolution'),
            docW = doc.getProperty('width') * docRes / 72,
            docH = doc.getProperty('height') * docRes / 72;
        activeDocument.suspendHistory('Find floor line', 'function() {}')
        activeDocument.suspendHistory('Get left stripe', 'measureDocument(floorY)')
        doc.stepBack();
        activeDocument.suspendHistory('Get right stripe', 'measureDocument(floorY)')
        doc.stepBack();
        activeDocument.suspendHistory('Rotate Document', 'rotateDocument()')
    }
} catch (e) { alert('A lot of things can go wrong in this script. :(\n\n' + e) }
function measureDocument(floor) {
    doc.flatten()
    doc.convertToGrayscale();
    floor.length ? doc.selectStrip(docH * 0.5, 0, docH, 1) : doc.selectStrip(docH * 0.5, docW - 1, docH, docW);
    doc.crop();
    var f = new File(Folder.temp + '/colors.raw');
    doc.saveToRAW(f)
    floor.push(findFloor(f));
}
function rotateDocument() {
    var title = lr.getProperty('name'),
        id = lr.getProperty('layerID')
    lr.duplicate(title)
    lr.deleteLayer(id);
    lr.rotate(Math.atan2(floorY[1] - floorY[0], docW) * 180 / Math.PI)
}
function findFloor(f) {
    var content = '';
    if (f.exists) {
        f.open('r');
        f.encoding = "BINARY";
        content = f.read();
        f.close();
        f.remove();
        var colors = function (s) {
            var m = 0, c = [];
            for (var i = 0; i < s.length; i++) {
                var k = s.charCodeAt(i); m += k; c.push(k)
            };
            return c
        }(content),
            dE = function (a, b) {
                return Math.round(Math.sqrt(Math.pow(a - b, 2)));
            },
            cur = 0,
            colorDiff = [];
        do {
            var stripe = [],
                median = 0;
            for (cur; cur < colors.length; cur++) {
                median += colors[cur]
                stripe.push(colors[cur])
                if (stripe.length >= STRIPE_SIZE) { cur++; break };
            }
            median = median / stripe.length
            for (var i = 0; i < stripe.length; i++) {
                colorDiff.push(dE(stripe[i], median))
            }
            if (cur == colors.length) break;
        } while (true)

        var max = 0,
            height = 0;
        for (var i = 0; i < colorDiff.length; i++) {
            if (colorDiff[i] >= max) { max = colorDiff[i]; height = i };
        }
        for (var i = height; i >= 0; i--) {
            if (dE(colors[height], colors[i]) > MAX_DE) return i + 1;
        }

    }
    return height;
}
function AM(target) {
    var s2t = stringIDToTypeID,
        t2s = typeIDToStringID;
    target = target ? s2t(target) : null;
    this.getProperty = function (property, id, idxMode) {
        property = s2t(property);
        (r = new ActionReference()).putProperty(s2t('property'), property);
        id != undefined ? (idxMode ? r.putIndex(target, id) : r.putIdentifier(target, id)) :
            r.putEnumerated(target, s2t('ordinal'), s2t('targetEnum'));
        return getDescValue(executeActionGet(r), property)
    }
    this.hasProperty = function (property, id, idxMode) {
        property = s2t(property);
        (r = new ActionReference()).putProperty(s2t('property'), property);
        id ? (idxMode ? r.putIndex(target, id) : r.putIdentifier(target, id))
            : r.putEnumerated(target, s2t('ordinal'), s2t('targetEnum'));
        return executeActionGet(r).hasKey(property)
    }
    this.convertToGrayscale = function () {
        (d = new ActionDescriptor()).putClass(s2t("to"), s2t("grayscaleMode"));
        executeAction(s2t("convertMode"), d, DialogModes.NO);
    }
    this.selectStrip = function (top, left, bottom, right) {
        (r = new ActionReference()).putProperty(s2t("channel"), s2t("selection"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        (d1 = new ActionDescriptor()).putUnitDouble(s2t("top"), s2t("pixelsUnit"), top);
        d1.putUnitDouble(s2t("left"), s2t("pixelsUnit"), left);
        d1.putUnitDouble(s2t("bottom"), s2t("pixelsUnit"), bottom);
        d1.putUnitDouble(s2t("right"), s2t("pixelsUnit"), right);
        d.putObject(s2t("to"), s2t("rectangle"), d1);
        executeAction(s2t("set"), d, DialogModes.NO);
    }
    this.flatten = function () {
        executeAction(s2t("flattenImage"), new ActionDescriptor(), DialogModes.NO);
    }
    this.crop = function () {
        (d = new ActionDescriptor()).putBoolean(s2t("delete"), true);
        executeAction(s2t("crop"), d, DialogModes.NO);
    }
    this.saveToRAW = function (f) {
        (d = new ActionDescriptor()).putBoolean(s2t('copy'), true);
        (d1 = new ActionDescriptor()).putObject(s2t("as"), s2t("rawFormat"), d);
        d1.putPath(s2t("in"), f);
        executeAction(s2t("save"), d1, DialogModes.NO);
    }
    this.stepBack = function () {
        (r = new ActionReference()).putEnumerated(charIDToTypeID('HstS'), s2t('ordinal'), s2t('previous'));
        (d = new ActionDescriptor()).putReference(s2t('null'), r);
        executeAction(s2t('select'), d, DialogModes.NO);
    }
    this.duplicate = function (title) {
        (r = new ActionReference()).putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        d.putString(s2t("name"), title);
        executeAction(s2t("duplicate"), d, DialogModes.NO);
    }
    this.deleteLayer = function (id) {
        (r = new ActionReference()).putIdentifier(s2t("layer"), id);
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        executeAction(s2t("delete"), d, DialogModes.NO);
    }
    this.rotate = function (angle) {
        (r = new ActionReference()).putEnumerated(s2t("layer"), s2t("ordinal"), s2t("targetEnum"));
        (d = new ActionDescriptor()).putReference(s2t("null"), r);
        d.putEnumerated(s2t("freeTransformCenterState"), s2t("quadCenterState"), s2t("QCSAverage"));
        d.putUnitDouble(s2t("angle"), s2t("angleUnit"), angle);
        d.putEnumerated(s2t("interfaceIconFrameDimmed"), s2t("interpolationType"), s2t("bicubic"));
        executeAction(s2t("transform"), d, DialogModes.NO);
    }
    function getDescValue(d, p) {
        switch (d.getType(p)) {
            case DescValueType.OBJECTTYPE: return { type: t2s(d.getObjectType(p)), value: d.getObjectValue(p) };
            case DescValueType.LISTTYPE: return d.getList(p);
            case DescValueType.REFERENCETYPE: return d.getReference(p);
            case DescValueType.BOOLEANTYPE: return d.getBoolean(p);
            case DescValueType.STRINGTYPE: return d.getString(p);
            case DescValueType.INTEGERTYPE: return d.getInteger(p);
            case DescValueType.LARGEINTEGERTYPE: return d.getLargeInteger(p);
            case DescValueType.DOUBLETYPE: return d.getDouble(p);
            case DescValueType.ALIASTYPE: return d.getPath(p);
            case DescValueType.CLASSTYPE: return d.getClass(p);
            case DescValueType.UNITDOUBLE: return (d.getUnitDoubleValue(p));
            case DescValueType.ENUMERATEDTYPE: return { type: t2s(d.getEnumerationType(p)), value: t2s(d.getEnumerationValue(p)) };
            default: break;
        };
    }
}

 

 


 

 
 

 

 

6 replies

Inspiring
October 13, 2025

Can you make the "after" picture you are looking for manually and post it here please?

Andrew Bold
Known Participant
October 13, 2025

Hello. The issue has been resolved.
At the beginning of the thread, there are examples, followed by a ready-to-use script.

Known Participant
February 8, 2025

I wish cameras would record gyroscope and accelerometer data in photo metadata so that we could quickly level photos with a single click during editing.

Legend
January 12, 2023

Well, you can't. Either something gets chopped off or there is distortion. You may be able to manually adjust and have transparent areas on the corners, that would require manual fill. Or you can manually use Perspective/Puppet Warp, those are powerful but can be tricky and slow.

 

Next time hold your camera straight and give plenty of room around the subject for cropping if needed.

Andrew Bold
Known Participant
January 12, 2023

I chose an unfortunate example (the picture with the girl). Don't take it as a rule.
99.9% of photos don't have cropped legs 🙂
In this case, you don't need to pay attention to it.

The transparent parts I can easily remove with action.

The main thing I'm interested in now is just to rotate the picture, without cropping, without deformation and distortion 😞

Andrew Bold
Known Participant
January 12, 2023

I do not know how to work with java script, but I have some ideas.
If there are people here who understand this, tell me if it is realistic to implement, or am I mistaken)

 

Here's my guess:

1. To align the image, we need to find 2 points, connect them with a line, and find out what is the angle between this line and the horizon. This value will be the angle by which we need to rotate the original image.

 

2. A similar method, we find 2 points and compare the distance from them to the edge of the canvas. The scrip should find the difference in distance and rotate the original image by half of this difference? (L1-L2)\2.
"I'm very bad at geometry too :), so don't judge me harshly."

 

3. I found the third method on the Internet.
It uses several methods, such as:
- Canny edge detector (https://en.wikipedia.org/wiki/Canny_edge_detector)
- Hough transform (https://en.wikipedia.org/wiki/Hough_transform)

In the original this article is in another language, so I tried to translate it and saved it in a pdf.
It describes step by step how this algorithm is implemented.

https://drive.google.com/drive/folders/14KnwoBWpd7lpVNgUBb-KTHGLJpKOTY3u?usp=sharing 

Andrew Bold
Known Participant
January 12, 2023

I think there are many ways in which we can find the extremes of "A" and "B."


For example:


1. Cut out the image inside except for a few pixels on the sides.
2. Using channels and levels to achieve a mask. Remove everything except the floor parts. And with the help of selection, make a stroke around these areas. As a result, we will have vector points, the coordinates of which can be used to calculate everything we need (?).

That's like the first option I had in my head to find vector coordinates, maybe I'm wrong about something.

 

Participant
August 27, 2023

Greetings @jazz-y !
Could you help me a bit with modifying this script?
I'm not very successful in modifying it.....

I'm trying to change this script for other photos (Different background, where there is no difference in brightness between wall and floor)


But in those photos, there is a clear black bar between the wall and the floor.

I thought it would be logical to include one "Threshold"(up 12 percent) correction step in the action. To get a clear black line.

After that, to modify the script so that the script detects the black line and not the absolute difference in brightness "findFloor".

But due to my poor knowledge of scripting I was not able to get a working result.

I will be very grateful if you have an opportunity to help me

Stephen Marsh
Community Expert
Community Expert
January 12, 2023
quote

Lightroom, camera raw are not up to the task.

 

By @Andrew Bold

 

Lr, ACR and the CR Filter in Photoshop was going to be my first and only suggestion (Geometry/Upright/Level).

 

Why isn't it up to the task?

 

quoteThe manual method, using a ruler, is not for me either.

 

Why?

 

quote

Perhaps there is some Script that can help me solve my problem?


Scripts can't perform magic, they need to leverage underlying features to correct geometry. For example, it is possible to script the Geometry/Upright/Vertical in the CR Filter in Photoshop. This would not use static values, it would be adaptive/variable for each image.

 

Andrew Bold
Known Participant
January 11, 2023

Conrad_C
Community Expert
Community Expert
January 12, 2023

The Upright feature in Camera Raw may be your best bet for two reasons: It can quickly correct multiple selected images in one click with no need for a script or action, and it can analyze each image and apply a different correct solution for each image. You need the second feature because the error is different in each picture.

 

And the error variations are challenging enough that only one Upright option I tried could fix both pictures with no manual intervention: The Full option. For some reason, the Level and Vertical options did something wrong on at least one of the images. But with the Full option, both images were corrected in one click, as shown in the demo below. Hopefully this will work as well on many more images than just these two. Then you could select any number of images and correct them all in one click.

 

 

The images were opened into Camera Raw from Adobe Bridge. Of course this works only if JPEG files are enabled for Camera Raw. (In Camera Raw preferences, File Handling, JPEG, HEIC, and TIFF Handling.) Camera Raw is also available as a filter in Photoshop, but using the filter version is not recommended because it works only for a layer in the current document, so you can’t load multiple images into the Camera Raw filter.

 

The Upright feature works the same way in Lightroom Classic, and you can batch-apply it using its Sync or Auto Sync features.

Andrew Bold
Known Participant
January 12, 2023

Hi. Thanks for the answers, but I wrote in the description that this method doesn't work for me.

 

I will explain why. There are several reasons:

  1. Straightening Camera Raw and Lightroom crops the image. I can't allow any part of the image to be lost, because often the legs will be cropped.
    2. Using this method, the picture is distorted, it changes the perspective. I need the picture to remain original and only change the angle of the picture.

 

I'm even fine with leaving transparent areas on the picture that appear when I tilt the image.