Skip navigation
Thenedus
Currently Being Moderated

Checking if a photo has adjustments

May 10, 2012 10:40 AM

Hi,

 

I am developing an export filter and I'm looking for a way to determine if an LRPhoto has any adjustments applied (i.e. if the "Photo has Develop adjustments" icon is displayed).

Since I couldn't find anything in the API, I am now using LRPhoto:getDevelopSettings() and check if some common settings are different from their default.

Of course, this is quite fragile, so I'd like to know if there is a better way to do that.

 
Replies
  • Currently Being Moderated
    May 10, 2012 11:51 AM   in reply to Thenedus

    Invoke catalog:findPhotos() with the criterion "hasAdjustments = true AND filename starts with <filename>", then test whether the photo is in the array of photos returned.  This should be pretty fast to invoke for each photo in question.  If not, you can invoke it on the entire catalog and cache the results.

     
    |
    Mark as:
  • Currently Being Moderated
    May 10, 2012 2:52 PM   in reply to Thenedus

    Dunno if has-adjustments is asserted simply for non-Adobe defaults (is it?). I assume it will be asserted if a develop preset is applied upon import.

     
    |
    Mark as:
  • Currently Being Moderated
    May 25, 2012 1:20 AM   in reply to Thenedus

    The solution works good, but has a severe performance impact.

    I tested it on 1500 photo's and my loop with and without increased from 11 seconds to 90 seconds.

     

    This is an expensive operation.

     

    Something tells me there should be a quicker way, but I have not found it yet.

     

    BTW why do you suggest  filename starts with <filename>" and not contains all?

     

    I used the code below where "name" is the name from the photo:

    local name = photo:getFormattedMetadata( "fileName" )

     

    local foundPhotos = catalog:findPhotos {

         searchDesc = {

            {

                criteria = "hasAdjustments",

                operation = "isFalse",

                value = true,

            },

            {

                criteria = "filename",

                operation = "all",

                value = name,

                value2 = "",

            },

            combine = "intersect",

         }

    }   

     
    |
    Mark as:
  • Currently Being Moderated
    May 25, 2012 1:30 AM   in reply to dhmc05

    Did you try John's 2nd idea (doing once for whole catalog), then convert the array returned into a lookup table, so you don't have to go searching through a big array over and over.

     

    ?

     
    |
    Mark as:
  • Currently Being Moderated
    May 25, 2012 1:34 AM   in reply to dhmc05

    dhmc05 wrote:

     

    local name = photo:getFormattedMetadata( "fileName" )

    Also, use the batch version. - it's performance improvement depends more on the number of photos than the number of data items.

     
    |
    Mark as:
  • Currently Being Moderated
    May 25, 2012 3:38 AM   in reply to Rob Cole

    Hi Rob,

     

    Still, I find it strange that in there is not a "hasAdjustments" in photo:getDevelopSettings(). That would solve this problem.

    I checked it with the debug toolkit from John Ellis.

     

    You are right, I should try the 2e solution. Thanks.

     

    My plugin loops through a group of selected photos (<= 1500). It checks if a photo has adjustments.

    A photographer college has 2 catalogs of each 150K. As far as I understand the catalog:findPhotos function works on an entire catalog. That would mean it would retrieve all photos with no adjustments.

    Probably that would be more than 100K for him. Would it then still be faster?

     
    |
    Mark as:
  • Currently Being Moderated
    May 25, 2012 6:48 AM   in reply to dhmc05

    Hi,

     

    You can do some benchmarking - John Ellis debugging toolkit is ideal for that purpose.

     

    But, findPhotos is generally quite fast compared to some other functions like those that get keywords. I assume it's wired fairly directly to the sqlite engine. But, John Ellis is the leading expert in this domain.

     

    If you expect most photos *will* have adjustments you could invert the sense of it.

     

    Let us know what you discover, OK?

     

    Cheers,

    Rob

     
    |
    Mark as:
  • Currently Being Moderated
    May 25, 2012 1:59 PM   in reply to Rob Cole

    I did a quick performance measurement, and using catalog:findPhotos() to get the "hasAdjustments" status of all photos of the catalog into a table is far faster than doing it one photo at a time. Details:

     

    We've discussed two variants:

     

    A. Extract the status of all photos from the catalog at once and put them in a table for fast access:

     

                hasAdjustments = false

     

    B. Extract the status one photo at a time using the "filename" criterion to limit the search.

     

                hasAdjustments = false AND filename startsWith <filename>

     

    I tested on a LR 3.6 catalog with 19K photos, 17K of which didn't have adjustments. Method A took a total of 0.14 seconds for the entire catalog, while method B took 0.2 seconds/ photo.  So clearly, method A is always better to use. 

     

    LR 4 has made many catalog operations faster.  In LR 4.1 RC2, method B took 0.02 seconds/photo, ten times faster.  But method A is still superior if you're handling more than a few photos.

     

    I think it's quite reasonable to expect that method A would still perform comparably on a catalog 10 times as large.  The resulting table would take just a few megabytes of memory.

     

    I agree that it would be more convenient if photo:getRawMetadata() supported "hasAdjustments".  But method A would still be faster, since catalog:batchGetRawMetadata() can only process a couple thousand photos per second, whereas method A goes two order of magnitudes faster.

     

    Here's the test script:

     

    
    --[[--------------------------------------------------------------------------
    Profile the performance of using catalog:findPhotos () to extract
    whether a photo has adjustments.
    ----------------------------------------------------------------------------]]
    local Require = require 'Require'.path ("../common")
    local Debug = require 'Debug'.init ()
    require 'strict'
    local LrApplication = import 'LrApplication'
    local LrFunctionContext = import 'LrFunctionContext'
    local Util = require 'Util'
    local catalog = LrApplication.activeCatalog ()
    local showErrors = Debug.showErrors
    local function findPhotosOnePhoto (filename)
        return catalog:findPhotos {
            searchDesc = {combine = "intersect",
                {criteria = "hasAdjustments", operation = "isFalse",
                 value = true},
                {criteria = "filename", operation = "startsWith", value = filename,
                 value2 = ""},
                {criteria = "filename", operation = "endsWith", value = filename,
                 value2 = ""}}}
        end
    local function getPhotoFilename (photo)
        return photo:getFormattedMetadata ("fileName")
        end
    local function findOne ()
        local nFound, nExamined = 0, 0
        local photos = catalog:getAllPhotos ()
        for i, photo in ipairs (photos) do  
            if i > 100 then break end
            local filename = getPhotoFilename (photo)
            local foundPhotos = findPhotosOnePhoto (filename)
            for i, foundPhoto in ipairs (foundPhotos) do
                nExamined = nExamined + 1
                if photo == foundPhoto then nFound = nFound + 1; break end
                end
            end
        return nFound, nExamined
        end
        
    local function findAll ()
        local foundPhotos = catalog:findPhotos {
            searchDesc = {combine = "intersect",
                {criteria = "hasAdjustments", operation = "isFalse",
                           value = true}}}
        local hasAdjustments = {}
        for i, photo in ipairs (foundPhotos) do
            hasAdjustments [photo] = true
            end
        return #foundPhotos
        end
        
    LrFunctionContext.postAsyncTaskWithContext ("test", showErrors (function (context)     
        findOne = Debug.profileFunc (findOne, "findOne")
        findAll = Debug.profileFunc (findAll, "findAll")
        findPhotosOnePhoto = Debug.profileFunc (findPhotosOnePhoto, 
                                                "findPhotosOnePhoto")
        getPhotoFilename = Debug.profileFunc (getPhotoFilename, "getPhotoFilename")
        Debug.logn ("findOne", findOne ())
        Debug.logn ("findAll", findAll ())
        Debug.logn (Debug.profileResults ())
        end))
     
    
     
    |
    Mark as:
  • Currently Being Moderated
    May 27, 2012 11:28 PM   in reply to John R. Ellis

    Results performance test

     

    With the above script I did the performance test on a 15k and 156k database.

     

    1) On a database with 15k photos:

    12/05/28 08:13:55com.johnrellis.logger     TRACE

    Function                       calls  top calls       time  time/call   time/top

    findPhotosOnePhoto               100        100    6.33760    0.06338    0.06338

    findAll                            1          1    0.05500    0.05500    0.05500

    findOne                            1          1    6.45510    6.45510    6.45510

    getPhotoFilename                 100        100    0.04750    0.00047    0.00047

     

    On a  database with 156k photos:

    12/05/28 08:16:29com.johnrellis.logger     TRACE

    Function                       calls  top calls       time  time/call   time/top

    findPhotosOnePhoto               100        100   70.19358    0.70194    0.70194

    findAll                            1          1    0.71001    0.71001    0.71001

    findOne                            1          1   71.38110   71.38110   71.38110

    getPhotoFilename                 100        100    0.40250    0.00403    0.00403

     

    Lightroom sytem info

    Lightroom version: 4.0 [814577]

    Operating system: Windows 7 Home Premium Edition

    Version: 6.1 [7601]

    Application architecture: x64

    System architecture: x64

    Physical processor count: 8

    Processor speed: 3,4 GHz

    Built-in memory: 8173,1 MB

    Real memory available to Lightroom: 8173,1 MB

     
    |
    Mark as:
  • Currently Being Moderated
    May 28, 2012 7:19 AM   in reply to dhmc05

    Very interesting -- confirms that method A is practical on large catalogs.

     

    Also, it looks like there is a performance anomaly with LR's use of the database with method B.  In the application, you can do

     

    filename starts with <filename>

     

    and the results for the entire catalog appear very quickly, indicating that there is a text index on that field. But the timings of catalog:findPhotos() in method B are not providing the same performance.   Not worth pursuing, though.

     
    |
    Mark as:
  • Currently Being Moderated
    Nov 19, 2013 5:02 AM   in reply to dhmc05

    At the risk of being forever branded as a hacker (mostly joking), another option is to:

     

    Compare each setting to default value.

     

    If you start with mostly likely to be edited first, it'll be very quick if most photos are so edited, e.g.

     

    local function hasAdjustments( photo )
        local settings = photo:getDevelopSettings()
        if settings.ProcessVersion == '6.7' then
            if settings.Exposure2012 ~= 0 then return true end
            -- etc.
        else -- legacy
            if settings.Exposure ~= 0 then return true end
            -- etc.
        end
        return false
    end
    

     

    And this may afford some other advantages - since default settings in Lr can be changed, it might be worth knowing whether they are different relative to current defaults, or relative to initial Adobe defaults, or if one applies a preset upon import, relative to photos with only that preset applied...

     

    PS - here is my method for getting Adobe default settings:

     

    --- Get Adobe default settings, for reset purposes.
    function DevelopSettings:getAdobeDefaultSettings( fmt, pv )
        local adj = {}
        -- pv hard-coded a.t.m.
        local raw
        if fmt == 'raw' then
            raw = true
            adj.WhiteBalance = "As Shot" -- no change to temperature and tint.
        elseif fmt == 'rgb' then
            -- hardly matters what white-balance is set at, when incremental values are zero, i.e. as-shot is essentially the same as zero.
            adj.IncrementalTemperature = 0
            adj.IncrementalTint = 0
        else
            app:callingError( "bad fmt: ^1", fmt )
        end
        
        local pv2012
        if pv == nil then
            pv2012 = true
        elseif self:isPv2012( pv ) then
            pv2012 = true
        elseif self:isLegacy( pv ) then
            pv2012 = false
        else
            app:callingError( "Bad pv: ^1", pv )
        end
        
        -- Basics (less WB)
        if pv2012 then
            adj.Exposure2012 = 0
            adj.Contrast2012 = 0
            adj.Highlights2012 = 0
            adj.Shadows2012 = 0
            adj.Whites2012 = 0
            adj.Blacks2012 = 0
            adj.Clarity2012 = 0
        else
            adj.Exposure = 0
            adj.Brightness = 50
            adj.Contrast = 25
            adj.FillLight = 0
            adj.HighlightRecovery = 0
            adj.Blacks = 5
            adj.Clarity = 0
        end     adj.Vibrance = 0     adj.Saturation = 0         -- Parametric curve.     adj.ParametricShadowSplit = 25     adj.ParametricMidtoneSplit = 50     adj.ParametricHighlightSplit = 75     adj.ParametricShadows = 0     adj.ParametricDarks = 0     adj.ParametricLights = 0     adj.ParametricHighlights = 0         -- Point curves.     if pv2012 then         adj.ToneCurvePV2012 = { 0, 0, 255, 255 }         adj.ToneCurvePV2012Red = { 0, 0, 255, 255 }         adj.ToneCurvePV2012Green = { 0, 0, 255, 255 }         adj.ToneCurvePV2012Blue = { 0, 0, 255, 255 }     else         adj.ToneCurve = { 0, 0, 255, 255 }     end         -- HSL     adj.HueAdjustmentRed = 0     adj.HueAdjustmentOrange = 0     adj.HueAdjustmentYellow = 0     adj.HueAdjustmentGreen = 0     adj.HueAdjustmentAqua = 0     adj.HueAdjustmentBlue = 0     adj.HueAdjustmentPurple = 0     adj.HueAdjustmentMagenta = 0     adj.SaturationAdjustmentRed = 0     adj.SaturationAdjustmentOrange = 0     adj.SaturationAdjustmentYellow = 0     adj.SaturationAdjustmentGreen = 0     adj.SaturationAdjustmentAqua = 0     adj.SaturationAdjustmentBlue = 0     adj.SaturationAdjustmentPurple = 0     adj.SaturationAdjustmentMagenta = 0     adj.LuminanceAdjustmentRed = 0     adj.LuminanceAdjustmentOrange = 0     adj.LuminanceAdjustmentYellow = 0     adj.LuminanceAdjustmentGreen = 0     adj.LuminanceAdjustmentAqua = 0     adj.LuminanceAdjustmentBlue = 0     adj.LuminanceAdjustmentPurple = 0     adj.LuminanceAdjustmentMagenta = 0         -- B&W     adj.GrayMixerRed = 0     adj.GrayMixerOrange = 0     adj.GrayMixerYellow = 0     adj.GrayMixerGreen = 0     adj.GrayMixerAqua = 0     adj.GrayMixerBlue = 0     adj.GrayMixerPurple = 0     adj.GrayMixerMagenta = 0         -- Splits     adj.SplitToningHighlightHue = 0     adj.SplitToningHighlightSaturation = 0     adj.SplitToningBalance = 0     adj.SplitToningShadowHue = 0     adj.SplitToningShadowSaturation = 0         -- Detail     if raw then         adj.Sharpness = 25         adj.ColorNoiseReduction = 25     else         adj.Sharpness = 0         adj.ColorNoiseReduction = 0     end     adj.SharpenRadius = 1     adj.SharpenDetail = 25     adj.SharpenEdgeMasking = 0     adj.LuminanceSmoothing = 0     adj.LuminanceNoiseReductionDetail = 50     adj.LuminanceNoiseReductionContrast = 0     adj.ColorNoiseReductionDetail = 50     adj.ColorNoiseReductionSmoothness = 50         -- LC     adj.LensProfileEnable = 0     adj.LensProfileSetup = "Default"     -- these may not be 'zactly what Lr would do upon reset, but hopefully won't blow the deal, since profile corrections will be disabled. ###4     -- I wouldn't have any idea what else to set them to, so...     -- reminder: Make & Model are derivatives that are neither readable nor settable via SDK, @28/Sep/2013 12:19.     -- adj.LensProfileName = ""     -- adj.LensProfileFilename = ""     adj.LensProfileDistortionScale = 0     adj.LensProfileVignettingScale = 0     adj.AutoLateralCA = 0     adj.DefringePurpleAmount = 0     adj.DefringePurpleHueLo = 30     adj.DefringePurpleHueHi = 70     adj.DefringeGreenAmount = 0     adj.DefringeGreenHueLo = 40     adj.DefringeGreenHueHi = 60     adj.LensManualDistortionAmount = 0     adj.PerspectiveVertical = 0     adj.PerspectiveHorizontal = 0     adj.PerspectiveRotate = 0     adj.PerspectiveScale = 0     adj.CropConstrainToWarp = 0     adj.VignetteMidpoint = 50     adj.VignetteAmount = 0         -- Effects     adj.PostCropVignetteStyle = 1 -- 1 is highlight-priority; 2 is color priority; 3 is paint-overlay (zero will also impose highlight priority, as opposed to being ignored).     adj.PostCropVignetteAmount = 0     adj.PostCropVignetteMidpoint = 50     adj.PostCropVignetteRoundness = 0     adj.PostCropVignetteFeather = 50     adj.PostCropVignetteHighlightContrast = 0     adj.GrainAmount = 0     adj.GrainSize = 25     adj.GrainFrequency = 50     -- Camera Calibration     if pv2012 then         adj.ProcessVersion = "6.7"     else         adj.ProcessVersion = "5.7" -- pv2010.     end     if raw then         adj.CameraProfile = "Adobe Standard"     else         adj.CameraProfile = "Embedded"     end     adj.ShadowTint = 0     adj.RedHue = 0     adj.RedSaturation = 0     adj.GreenHue = 0     adj.GreenSaturation = 0     adj.BlueHue = 0     adj.BlueSaturation = 0         -- Ena/Dis. - not doing.         adj.RetouchInfo = {}     adj.RetouchAreas = {}     adj.RedEyeInfo = {}     adj.GradientBasedCorrections = {}     adj.CircularGradientBasedCorrections = {}     adj.PaintBasedCorrections = {}     -- reminder: if settings aren't recognized, they're ignored.         return adj end

     

    In case you haven't noticed, there is no SDK support for resetting a photo either .

     

    Rob

     
    |
    Mark as:
  • Currently Being Moderated
    Nov 20, 2013 8:08 PM   in reply to Rob Cole

    FWIW, I just discovered this morsel in use for my own purposes:

     

    local ds = photo:getDevelopSettings()
    local pvNum = tonumber( ds.ProcessVersion )
    local basics = true
    if pvNum > 6 then -- pv12
        repeat
            if ds.Exposure2012 ~= 0 then break end
            if ds.Contrast2012 ~= 0 then break end
            if ds.Highlights2012 ~= 0 then break end
            if ds.Shadows2012 ~= 0 then break end
            if ds.Whites2012 ~= 0 then break end
            if ds.Blacks2012 ~= 0 then break end
            if ds.Clarity2012 ~= 0 then break end
            basics = false -- all 0
        until true
        if basics then
            app:logV( "PV2012 basics are adjusted." )
        else
            unfin[#unfin + 1] = "no pv12 basic adjustments"
        end
    else -- legacy
        --[[
        repeat
            if ds.Exposure ~= 0 then break end
            if ds.Contrast ~= 0 then break end
            if ds.FillLight ~= 0 then break end
            if ds.HighlightRecovery ~= 0 then break end
            if ds.Brightness ~= 0 then break end
            if ds.Blacks ~= 0 then break end
            if ds.Clarity ~= 0 then break end
            basics = false -- all 0
        until true
        --]]
        unfin[#unfin + 1] = "process version is not current" -- If I'm gonna bother to export, may as well edit in current process version, or be prompted to OK it.
    end
    

     

    In my Lr life, I always set white balance to custom immediately upon import, since it helps things (long story), and I always adjust white balance by eye anyway... So, all my photos are considered "adjusted" by Lr, thus the 'hasAdjustments' metadata is meaningless.

     

    Thus, the above code works out better - I always adjust the basics if I adjust anything, even if perfect at all zeros, I'll set exposure to .01 as a "flag" meaning "basics OK at zero".

     

    Anyway, I dunno what the purpose is for figuring "is adjusted", but it's worth a few levels in my view, i.e. "how adjusted" may be more useful than "is adjusted"...

     

    Whatever,

    Rob

     
    |
    Mark as:

More Like This

  • Retrieving data ...

Bookmarked By (0)

Answers + Points = Status

  • 10 points awarded for Correct Answers
  • 5 points awarded for Helpful Answers
  • 10,000+ points
  • 1,001-10,000 points
  • 501-1,000 points
  • 5-500 points