Hi all,

I have a python script I wrote to do this. I don't know if file attachments
work for this mailing list, but let's find out.

Usage: If you want to look at focal length for all .jpg files in the
current folder:
$ python /path/to/PhotoAnalyze.py fl *.jpg

It will generate a histogram. You can click and drag to define a range and
it will write a summary file listing the file names of the images whose
focal length fell within the range you dragged. For example, suppose I want
to view a slide show of all the images taken at a focal length above 100
mm, I would click at 100 mm and drag off the right end of the histogram.
Other properties you can plot aside from focalLength (fl) are: aperture,
iso, shutterSpeed (ss).

The little radio buttons on the left side of the histogram specify the dot
extension for the file list that is written out. For example, I usually run
the analysis on my .NEF files, but perhaps I want to look at a slideshow of
all the .jpg files that correspond to the range I select. The script uses
the extension ".jpg" for each of the file names in the list instead of
".NEF."

I would very much like to write this sort of functionality into darktable,
since then it could take advantage of darktable's powerful ability to
generate collections based on arbitrary criteria (my script is most
convenient if all the files you want to compare are in the same folder).

Question: What would be the best thing to use to make this a darktable
plugin? Lua? Does lua (or darktable) have some built-in concepts for
histograms, for example? (I don't know if adapting darktable's exposure
histogram is the best way to do this).

Regards,

Owen


On Sat, Apr 16, 2016 at 2:36 PM, Rav <[email protected]> wrote:

> Hi!
>
> It woulb be awesome to gave stats on your camera usage like how often you
> use 16mm, iso100  or f/8.
>
> Best regards,
>
> Rav
> Le 16 avr. 2016 19:18, <[email protected]> a écrit :
>
>> Hi,
>>
>> DT 2.0.3.
>>
>> Is there a simple way of getting stats/summary of focal
>> lens used in a collection/film roll?
>>
>> or do I need to do that in exiftool?
>>
>> --
>> sknahT
>>
>> vyS
>>
>> ____________________________________________________________________________
>> darktable user mailing list
>> to unsubscribe send a mail to
>> [email protected]
>>
>>
> ____________________________________________________________________________
> darktable user mailing list to unsubscribe send a mail to
> [email protected]
>

____________________________________________________________________________
darktable user mailing list
to unsubscribe send a mail to [email protected]

# Copyright 2016 by Owen Mays
# [email protected]
# Provided as-is, no warranty of any kind.

#!/usr/bin/python
import time
import os # for file I/O
import sys # allows for an exit if wrong parameters are passed
import exifread # for reading Exif's :-D
import argparse # for parsing input arguments (including the file list)
import matplotlib.pyplot as plt # plotting
from matplotlib.widgets import RadioButtons #For radio buttons!

import numpy as np # logs and logarithmic intervals (for ISO)
from subprocess import call # for executing command-line commands

#Line object for beginning and end of selection
global startLine
startLine=None
global endLine
endLine=None

#File extension for summary file output
global summaryFileExtension
summaryFileExtension='jpg'


parser = argparse.ArgumentParser()
parser.add_argument("property",nargs=1,help="Specify the property to plot [focalLength (or fl), iso, ShutterSpeed (or ss), aperture]")
parser.add_argument("files",nargs='+',help="Specify a list of files.")
args = parser.parse_args()


desiredData=np.array([]) #not using a default list b/c that would not allow element-wise math operations.
keyErrorCounter=0

if args.property is None:
    print("Must specify a property")
    sys.exit()

desiredProperty=args.property
if(len(desiredProperty) != 1):
    print("Cannot specify more than one property to print")
    sys.exit()

desiredProperty=desiredProperty[0].casefold() # more robust than casting to lowercase

# Figure out which property was requested

# Focal length
if (desiredProperty=='fl' or desiredProperty=='focallength'):
    # Parameters for reading the EXIF
    stopTag='FocalLength' # stop reading the EXIF header after the fl is found.
    tagKey='EXIF FocalLength' #to get something out of the tags dictionary
    # Parameters for plotting
    title="Focal Lengths"
    xLabel="Focal Length [mm]"
    preUnits=""
    postUnits="mm" # to facilitate automatically printing "iso 400" or "35 mm"

    bins=range(17,202) # bin edges 17-201 inclusive. Range is exclusive, thus I specify 202 as the end point.
    xTicks=np.array(range(15,205,5))
    xTickLabels=np.char.mod('%d',xTicks)
    xScale='linear'

# ISO
if (desiredProperty=='iso'):
    # Parameters for reading the EXIF
    stopTag='ISOSpeedRatings' # stop reading the EXIF header after the fl is found.
    tagKey='EXIF ISOSpeedRatings' #to get something out of the tags dictionary
    # Parameters for plotting
    title="ISO Values"
    xLabel="ISO Sensitivity"
    preUnits="iso"
    postUnits=""
    bins=np.array(range(0,8)) #these are going to be bin edges...and we're interested in 100*powers of 2 being evenly spaced (100,200,400,800, etc)
    bins=bins-.5
    bins=100*2**bins

    xTicks=np.array([100,200,400,800,1600,3200,6400])
    xTickLabels=np.char.mod('%d',xTicks) # gives an array of strings
    xScale='log'



# shutter speed
if (desiredProperty=='ss' or desiredProperty=='shutterspeed'):
    stopTag='ExposureTime' # this will be a ratio, and we want the denominator, watch out!
    tagKey='EXIF ExposureTime'
    title="Exposure Times"
    xLabel="Exposure Time [seconds]"
    preUnits=""
    postUnits="s"
    bins=np.logspace(0,3,num=100)

    xTicks=np.array([1,2,4,8,15,30,60,120,250,500,1000])
    xTickLabels=np.char.mod('1/%d',xTicks)
    xScale='log'

if (desiredProperty=='aperture'):
    stopTag='Aperture'
    tagKey='EXIF FNumber' #Ratios, but can be converted to ints.
    title='Aperture'
    xLabel="F-number"
    preUnits="f/"
    postUnits=""

    #Still not super happy with this spacing...
    bins= np.array([1,1.3,1.7,2,2.3,2.7,3,3.3,3.7,4,4.3,4.7,5,5.3,5.7,6,6.3,6.7,7,7.3,7.7,8,8.3]) #AV; f/N = sqrt(2**AV), from the wikipedia page on f/number
    bins=bins-.15;
    bins=np.sqrt((2**bins))
    print(bins)

    xTicks=np.array([1.4,1.6,1.8,2.0,2.2,2.5,2.8,3.2,3.5,4,4.5,5,5.6,6.3,7.1,8,11,16])
    xTickLabels=np.char.mod('f/%.1f',xTicks)
    xScale='log'

for i, fileName in enumerate(args.files): #i is a dummy, enumerate returns a tuple
    f = open(fileName, 'rb')
    tags = exifread.process_file(f, details=False,stop_tag=stopTag)
    # EXIFread stores things in its own stupid Ratio datatype.
    # tags['EXIF FocalLength'] is the tag we want.
    # desiredData.values[0] is the ratio object holding the focal length.
    # ratio.num is the numerator, ratio.den is the denominator.
    # So all in one line, the following extracts numerator, denominator, does the division and appends that float to the desiredData list

    print(fileName)

    try:
        if (type(tags[tagKey].values[0]) is exifread.utils.Ratio): # if it's exifread's stupid ratio datatype
            if (stopTag=='ExposureTime'): #then we want the denominator
                desiredData=np.append(desiredData,float(tags[tagKey].values[0].den))
            else:
                desiredData=np.append(desiredData,float(tags[tagKey].values[0].num/tags[tagKey].values[0].den))
        elif (type(tags[tagKey].values[0]) is int):
            desiredData=np.append(desiredData,tags[tagKey].values[0])
        else:
            print("Attribute was not a ratio or an int. Exiting.")
            sys.exit()
    except KeyError:
        keyErrorCounter+=1 # sometimes corrupted headers cause errors. I ignore and just count the failures, no sense breaking the analysis of a huge folder.


print(keyErrorCounter," key errors (this many files were skipped).")

fig = plt.figure()

plt.hist(desiredData, bins=bins)
histogramAxes=plt.axes()
plt.title(title)
plt.xlabel(xLabel)
plt.ylabel("Count")
plt.xscale(xScale)
plt.xticks(xTicks,xTickLabels) # Note that I'm over-writing the x labels with strings to make the ISO display work.
                               # This is dangerous! The values displayed on the x-axis are decoupled from the data!
                               # If you screw up those strings, the values displayed at the tick marks have NOTHING to do with the data.


plt.xlim(min(bins), max(bins)) # Avoid lots of white space between 0 and 100.

# Add jpg/raw radio button
buttonAx=plt.subplot(111)
rax=plt.axes(plt.axes([0.01,0.2,.07,.07]))

extensionRadio=RadioButtons(rax,('jpg','NEF','CR2'))


#what to do when the extension radio button is clicked
def changeExtension(label):
    global summaryFileExtension
    summaryFileExtension=label
    print(summaryFileExtension)

extensionRadio.on_clicked(changeExtension)



# plt.subplot(212)
# plt.hist(desiredData,bins=(range(18,201)),normed=True,cumulative=True)
# plt.xlabel("Focal Length [mm]")
# plt.ylabel("Total below")
# plt.xticks(range(15,200,5))



# Function to determine which bin a given coordinate belongs to
def getBinLowerEdge(xCoord,bins):
    for binNum in range(0,len(bins)):
        if xCoord <= bins[binNum]:
            if xCoord>bins[binNum-1]:
                return (binNum-1)
                break
            else:
                print("error putting ",xCoord," into a bin")
                print("bins[binNum-1]=",bins[binNum-1])
                print("bins[binNum]=",bins[binNum])
                sys.exit()


# Generate list of filenames between two points on the histogram

def generateSummary():
    global summaryFileExtension
    global xStartBin
    global xEndBin

    it=np.nditer(desiredData, flags=['f_index'])

    print("\n\n")

    #Generate a summary file name based on the selected range, e.g. 'SummaryFile_17-55mm.txt' or 'SummaryFile_ISO100-800.txt'

    summaryFileNameString='SummaryFile_{}{}-{}{}.txt'.format(preUnits.replace('/',''),str(bins[xStartBin]),str(bins[xEndBin]),postUnits)

    #If the summary file name exists, clear it:
    if(os.path.exists(summaryFileNameString)):
        open(summaryFileNameString,'w').close()

    summaryFile=open(summaryFileNameString,'w')
    print(summaryFileExtension)
    while not it.finished:
        if(bins[xStartBin] <= it[0] and it[0] <= bins[xEndBin]): #it[0] is the value in that location
            print(args.files[it.index],"=> ",preUnits,it[0],postUnits)
            summaryFile.write("{}.{}\n".format(os.path.splitext(args.files[it.index])[0],summaryFileExtension))
        it.iternext()

# Handle mouse clicks
def on_click(event):
    global xStartBin
    global startLine
    global endLine
    global firstX #first x-coord clicked. user might want to drag right-to-left.

    if event.inaxes is histogramAxes: #Don't try to do anything with data processing if the user's clicking in the radio buttons.
        firstX=event.xdata
        if startLine is not None:
            startLine.set_xdata(event.xdata)
        else:
            startLine=histogramAxes.axvline(event.xdata,color='k')
        if endLine is not None: # this serves to "remove" the old end-line
            endLine.set_xdata(event.xdata)



        xStartBin = getBinLowerEdge(event.xdata,bins)
        plt.draw()

    else:
        print("Clicked outside axis bounds but inside plot window")


def on_release(event):
    global firstX
    global xStartBin
    global xEndBin
    global startLine
    global endLine

    #Only do data processing and line drawing if the user clicks inside the histogram area. Radio buttons handle themselves.
    if event.inaxes is histogramAxes:

        xEndBin = getBinLowerEdge(event.xdata,bins)+1

        if (event.xdata <= firstX): # user dragged right-to-left. Bin edge snapping needs to be flipped
            xEndBin=xEndBin-1
            xStartBin=xStartBin+1
            xStartBin, xEndBin = xEndBin, xStartBin #"start" should be at the lower value


        if endLine is not None:
            endLine.set_xdata(bins[xEndBin])
        else:
            endLine=histogramAxes.axvline(bins[xEndBin],color='k')

        startLine.set_xdata(bins[xStartBin]) #make sure both lines snap to the edges of the bins

        plt.draw()

        generateSummary()

#        print("x dragged from ",bins[xStartBin]," to ",bins[xEndBin],"(bins ",xStartBin," to ",xEndBin,")")




fig.canvas.callbacks.connect('button_press_event',on_click)
fig.canvas.callbacks.connect('button_release_event',on_release)

plt.show()
plt.ion()

Reply via email to