Why are giotto-tda and cripser giving different persistent diagrams? When I find the persistence diagrams using cubical homology and using the natural grayscale filtration of the image, I get two different answers depending on the package I use. By inspection, it seems that the package cripser gives the expected persistence diagram, and giotto-tda gives a persistence diagram that does not make sense to me. My questions is, why do giotto-tda and cripser give different persistent diagrams?
Here I will give a reproducible example, and point out the differences in the persistence diagrams.
You can find instructions to download cripser here, and instructions to download giotto-tda are here.
First, cripser does not come with plotting functions, so I've made one here that you may use for the example below, but feel free to ignore it:
import numpy as np
import matplotlib.pyplot as plt
import cripser

def get_2d_pd(gray_image):
    '''Takes a 2d numpy array and produces the persistence diagram data
    in a format specified at https://github.com/shizuo-kaji/CubicalRipser_3dim#how-to-use'''
    return cripser.computePH(gray_image, maxdim=1)

def display_2d_pd(pd, disp_db_locs = False):
    b0 = np.array([x[1] for x in pd if x[0]==0])
    x0 = np.linspace(np.min(b0), np.max(b0))
    d0 = np.array([x[2] for x in pd if x[0]==0])

    d0[-1] = np.max(d0[:-1])*1.1 #make infinite death value 10% more than all other death values

    b1 = np.array([x[1] for x in pd if x[0]==1])
    x1 = np.linspace(np.min(b1), np.max(b1))
    d1 = np.array([x[2] for x in pd if x[0]==1])

    fig, ax = plt.subplots(1,2)
    ax[0].plot(x0, x0, 'k--')
    ax[0].scatter(b0, d0, color = 'b')
    ax[0].set_xlabel('Birth')
    ax[0].set_ylabel('Death')
    ax[0].set_title('0-D Persistent Homology')

    ax[1].plot(x1, x1, 'k--')
    ax[1].scatter(b1, d1, color = 'r')
    ax[1].set_xlabel('Birth')
    ax[1].set_ylabel('Death')
    ax[1].set_title('1-D Persistent Homology')

    if disp_db_locs:
        lbl0 = np.array([ [x[3], x[4], x[6], x[7]] for x in pd if x[0]==0])
        lbl0_dict = {}
        lbl1 = np.array([ [x[3], x[4], x[6], x[7]] for x in pd if x[0]==1])
        lbl1_dict = {}

        for i, lbls in enumerate(lbl0):
            pt = (b0[i], d0[i])
            if pt in lbl0_dict.keys():
                lbl0_dict[pt].append(lbls)
            else:
                lbl0_dict[pt] = [lbls]
                
        for pt, lbls in lbl0_dict.items():
            txt = ''
            for lbl in lbls:
                txt += '('+str(lbl[0])+', '+str(lbl[1])+'), ('+str(lbl[2])+', '+str(lbl[3])+') \n'
            ax[0].annotate(txt, pt)

        for i, lbls in enumerate(lbl1):
            pt = (b1[i], d1[i])
            if pt in lbl1_dict.keys():
                lbl1_dict[pt].append(lbls)
            else:
                lbl1_dict[pt] = [lbls]

        for pt, lbls in lbl1_dict.items():
            txt = ''
            for lbl in lbls:
                txt += '('+str(lbl[0])+', '+str(lbl[1])+'), ('+str(lbl[2])+', '+str(lbl[3])+') \n'
            ax[1].annotate(txt, pt)

    plt.show()

Here is the main example:
# Generate a random 20 by 20 array
from numpy.random import default_rng
rng = default_rng(1)
vals = rng.standard_normal((20,20))

#Plot a grayscale of the image
from gtda.plotting import plot_heatmap
import plotly.express as px
plot_heatmap(vals)

#Get persistence diagram using giotto-tda
from gtda.homology import CubicalPersistence
cubical_persistence = CubicalPersistence(n_jobs=-1)
rand_vals = cubical_persistence.transform(vals)
cubical_persistence.plot(rand_vals)

#Get persistence diagram using cripser and helper functions defined above
cripser_pd = get_2d_pd(vals)
display_2d_pd(cripser_pd)


Result from giotto-tda

Result from cripser

Original Image

Notable differences

*

*First, gtda does not detect any 1D homology while cripser does. Why?

*Second, for 0D homology, gtda detects many less components than cripser.

*Finally, the components that gtda does detect do not have the same birth and death values as the components detected by cripser.

Any help on clarifying why I have gotten two seemingly inconsistent outputs would be much appreciated!
 A: Ok, I'm not 100% this is the solution, but it's worth giving a shot. As you can see here here, there are two ways to build a filtered cubical complex on a grid, the V-construction (pixels are 0 cells, equivalent to 4-adjacency) and the T-construction (pixels are  top-dimensional cells, equivalent to 8-adjacency). Cripser apparently computes the V-construction by default, whereas Giotto, if it indeed uses GUDHI as backend, uses the T-construction, see here.
The paper Duality in Persistent Homology of Images
 shows that the duality between the persistence of the V- and T-constructions requires you to dualize the height function, which in this case just means replacing "vals" with "-1*vals", and the resulting persistence won't be exactly the same, but will be equivalent up to a beautiful symmetry (taken from their paper):

In summary, there are two, dual conventions for doing cubical persistence on grids, and the symmetry between them requires you to flip the height function and then flip/negate some of the bars across different homological degrees.
