Como faço para percorrer todas as células em uma varredura contínua?


13

Veja este link para mais detalhes.

O problema:

Quero fazer um loop através de uma varredura contínua (que não possui tabela de atributos), célula por célula, e obter o valor da célula. Eu quero pegar esses valores e executar condicionais neles, emulando as etapas de álgebra do mapa detalhadas abaixo sem usar a calculadora raster.

Por solicitação dos comentários abaixo, adicionei detalhes fornecendo informações básicas sobre o problema e justificando a necessidade de implementar um método como tal na seção abaixo chamada "A análise necessária:".

A análise proposta abaixo, apesar de ser relevante para o meu problema, fornecendo informações básicas, não precisa ser implementada em uma resposta. O escopo da pergunta refere-se apenas à iteração através de uma varredura contínua para obter / definir os valores da célula.

A análise necessária:

Se QUALQUER uma das seguintes condições for atendida, forneça um valor de 1 a célula de saída. Somente forneça um valor de 0 a célula de saída se nenhuma das condições for atendida.

Condição 1: se o valor da célula for maior que as células superior e inferior, forneça o valor 1:

Con("raster" > FocalStatistics("raster", NbrIrregular("C:\filepath\kernel_file.txt"), "MAXIMUM"), 1, 0)

Onde o arquivo do kernel se parece com isso:

3 3 
0 1 0
0 0 0
0 1 0

Condição 2: se o valor da célula for maior que as células esquerda e direita, forneça o valor 1:

Con("raster" > FocalStatistics("raster", NbrIrregular("C:\filepath\kernel_file.txt"), "MAXIMUM"), 1, 0)

Onde o arquivo do kernel se parece com isso:

3 3 
0 0 0
1 0 1
0 0 0  

Condição 3: se o valor da célula for maior que o topleft e as células inferiores, forneça o valor 1:

Con("raster" > FocalStatistics("raster", NbrIrregular("C:\filepath\kernel_file.txt"), "MAXIMUM"), 1, 0)

Onde o arquivo do kernel se parece com isso:

3 3 
1 0 0
0 0 0
0 0 1 

Condição 4: se o valor da célula for maior que as células inferior esquerda e superior direita, forneça o valor 1:

Con("raster" > FocalStatistics("raster", NbrIrregular("C:\filepath\kernel_file.txt"), "MAXIMUM"), 1, 0)

Onde o arquivo do kernel se parece com isso:

3 3 
0 0 1
0 0 0
1 0 0 

Condição 5: se qualquer uma das células adjacentes tiver um valor igual à célula central, atribua à varredura de saída o valor 1 ( usando variedade focal com os dois cálculos de vizinhança mais próximos )

Por que não usar álgebra de mapa?

Foi observado abaixo que meu problema poderia ser resolvido usando álgebra de mapa, mas, como visto acima, esse é um total geral de seis cálculos de varredura, mais um para combinar todos os rasters criados juntos. Parece-me que é muito mais eficiente ir célula por célula e fazer todas as comparações de uma só vez em cada célula, em vez de repetir cada uma individualmente sete vezes e utilizar bastante memória para criar sete rasters.

Como o problema deve ser atacado?

O link acima aconselha o uso da interface IPixelBlock, mas na documentação da ESRI não está claro se você está realmente acessando um valor de célula única através do IPixelBlock ou se está acessando vários valores de célula do tamanho do IPixelBlock definido. Uma boa resposta deve sugerir um método para acessar os valores das células de uma varredura contínua e fornecer uma explicação da metodologia por trás do código, se não aparentemente óbvio.

Em suma:

Qual é o melhor método para percorrer todas as células em uma varredura CONTÍNUA (que não possui tabela de atributos ) para acessar seus valores de célula?

Uma boa resposta não precisa implementar as etapas de análise descritas acima, mas apenas fornecer uma metodologia para acessar os valores das células de uma varredura.


4
É quase sempre desnecessário percorrer todas as células de uma varredura. Você pode fornecer mais informações sobre o que você está tentando fazer?
user2856

2
@Luke está correto: de longe, a melhor maneira de realizar um cálculo de varredura iterativo em qualquer GIS é evitar o loop explicitamente pelas células, porque sob o capô qualquer loop que precisa ser feito já foi otimizado. Em vez disso, procure uma maneira de usar a funcionalidade de álgebra de mapa fornecida pelo GIS, se isso for possível. Se você descrever sua análise, poderá obter respostas úteis que usem essa abordagem.
whuber

@ Lucas Adicionei detalhes da análise.
Conor

1
Obrigado pelo esclarecimento, Conor. Concordo que, se o seu GIS incorre em uma sobrecarga substancial para cada cálculo de varredura, escrever seu próprio loop pode ser mais eficiente. Por curiosidade, qual é a interpretação pretendida desse conjunto (incomum) de condições?
whuber

1
@whuber É para operações de detecção de borda criar polígonos vetoriais da minha varredura. O aplicativo é conceitualmente semelhante à identificação de bacias hidrológicas de um DEM (pense na célula central nas estatísticas de vizinhança listadas acima como o "pico" do qual a água fluiria ladeira abaixo), mas está fora do campo da hidrologia. Anteriormente, eu estava usando o Flow Direction e Basin Rasters para esse fim, mas eles são propensos a erros na minha análise final devido às propriedades desses métodos não serem exatamente o que eu preciso.
Conor

Respostas:


11

Vejo que isso já foi resolvido pelo Original Poster (OP), mas publicarei uma solução simples em python, caso alguém no futuro esteja interessado em diferentes maneiras de resolver esse problema. Sou parcial em relação ao software de código aberto, então aqui está uma solução usando GDAL em python:

import gdal

#Set GeoTiff driver
driver = gdal.GetDriverByName("GTiff")
driver.Register()

#Open raster and read number of rows, columns, bands
dataset = gdal.Open(filepath)
cols = dataset.RasterXSize
rows = dataset.RasterYSize
allBands = dataset.RasterCount
band = dataset.GetRasterBand(1)

#Get array of raster cell values.  The two zeros tell the 
#iterator which cell to start on and the 'cols' and 'rows' 
#tell the iterator to iterate through all columns and all rows.
def get_raster_cells(band,cols,rows):
    return band.ReadAsArray(0,0,cols,rows)

Implemente a função assim:

#Bind array to a variable
rasterData = get_raster_cells(band,cols,rows)

#The array will look something like this if you print it
print rasterData
> [[ 1, 2, 3 ],
   [ 4, 5, 6 ],
   [ 7, 8, 9 ]]

Em seguida, repita seus dados com um loop aninhado:

for row in rasterData:
    for val in row:
        print val
> 1
  2
  3
  4...

Ou talvez você queira nivelar sua matriz 2-D com uma compreensão da lista:

flat = [val for row in rasterData for val in row]

De qualquer forma, ao iterar os dados célula a célula, é possível lançar alguns condicionais em seu loop para alterar / editar valores. Veja este script que escrevi para diferentes maneiras de acessar os dados: https://github.com/azgs/hazards-viewer/blob/master/python/zonal_stats.py .


Gosto da simplicidade e elegância desta solução. Vou esperar mais alguns dias e, se ninguém mais encontrar uma solução de qualidade igual ou superior, adicionarei tags para ampliar o escopo da pergunta em benefício da comunidade e lhe conceder a recompensa.
Conor

Obrigado, @Conor! Encontramos um problema semelhante no meu local de trabalho no início desta semana e, por isso, resolvi-o escrevendo uma classe com GDAL / python. Especificamente, precisávamos de um método do lado do servidor para calcular o valor médio de uma área de uma varredura, considerando apenas uma caixa delimitadora de um usuário em nosso aplicativo do lado do cliente. Você acha que seria benéfico se eu adicionasse o restante da turma que escrevi?
asonnenschein

Adicionar código mostrando como ler a matriz 2-D recuperada e editar seus valores seria útil.
Conor

9

Atualizar! A solução numpy:

import arcpy
import numpy as np

in_ras = path + "/rastername"

raster_Array = arcpy.RasterToNumPyArray(in_ras)
row_num = raster_Array.shape[0]
col_num = raster_Array.shape[1]
cell_count = row_num * row_num

row = 0
col = 0
temp_it = 0

while temp_it < cell_count:
    # Insert conditional statements
    if raster_Array[row, col] > 0:
        # Do something
        val = raster_Array[row, col]
        print val
    row+=1
    if col > col_num - 1:
        row = 0
        col+=1

Portanto, retornar a matriz finalizada à varredura usando o arcpy é problemático. arcpy.NumPyArrayToRaster é um esquilo e tende a redefinir extensões, mesmo que você as alimente com as coordenadas LL.

Eu prefiro salvar como texto.

np.savetxt(path + "output.txt", output, fmt='%.10f', delimiter = " ")

Estou executando o Python como velocidade de 64 bits - no momento, isso significa que não posso alimentar o cabeçalho numpy.savetxt. Então eu tenho que abrir a saída e adicionar o cabeçalho ASCII que o Arc deseja antes de converter o ASCII para o Raster

File_header = "NCOLS xxx" + '\n'+ "NROWS xxx" + '\n' + "XLLCORNER xxx"+'\n'+"YLLCORNER xxx"+'\n'+"CELLSIZE xxx"+'\n'+"NODATA_VALUE xxx"+'\n'

A versão numpy executa minhas varreduras de varredura, multiplicações e acréscimos muito mais rapidamente (1000 iterações em 2 minutos) do que a versão arcpy (1000 iterações em 15 min)

VERSÃO ANTIGA Posso excluir isso mais tarde, acabei de escrever um script semelhante. Tentei converter para pontos e usando o cursor de pesquisa. Eu tenho apenas 5000 iterações em 12 horas. Então, eu procurei por outro caminho.

Minha maneira de fazer isso é iterar pelas coordenadas do centro da célula de cada célula. Começo no canto superior esquerdo e movo da direita para a esquerda. No final da linha, desço uma linha e começo de novo à esquerda. Eu tenho uma varredura de 240 m com 2603 colunas e 2438 linhas, para um total de 6111844 células totais. Eu uso uma variável iteradora e um loop while. Ver abaixo

Algumas notas: 1 - você precisa conhecer as coordenadas da extensão

2 - corra com coordenadas de ponto para o centro da célula - mova 1/2 do tamanho da célula dos valores de extensão

3 - Meu script está usando o valor da célula para extrair uma varredura específica de valor e, em seguida, mude essa varredura para o centro da célula original. Isso adiciona uma varredura zero para expandir a extensão antes de adicionar a uma varredura final. Este é apenas um exemplo. Você pode colocar suas instruções condicionais aqui (segunda instrução if dentro do loop while).

4 - Este script assume que todos os valores de varredura podem ser convertidos como números inteiros. Isso significa que você precisa se livrar primeiro dos dados. Con IsNull.

6 - Ainda não estou feliz com isso e estou trabalhando para tirar isso completamente do arcpy. Eu preferiria criar matrizes numpy e fazer as contas por lá e depois trazê-las de volta ao Arc.

ULx = 959415 ## coordinates for the Upper Left of the entire raster 
ULy = 2044545
x = ULx ## I redefine these if I want to run over a smaller area
y = ULy
temp_it = 0

while temp_it < 6111844: # Total cell count in the data extent
        if x <= 1583895 and y >= 1459474: # Coordinates for the lower right corner of the raster
           # Get the Cell Value
           val_result = arcpy.GetCellValue_management(inraster, str(x)+" " +str(y), "1")
           val = int(val_result.getOutput(0))
        if val > 0: ## Here you could insert your conditional statements
            val_pdf = Raster(path + "pdf_"str(val))
            shift_x  =  ULx - x # This will be a negative value
            shift_y = ULy - y # This will be a positive value
            arcpy.Shift_management(val_pdf, path+ "val_pdf_shift", str(-shift_x), str(-shift_y))
            val_pdf_shift = Raster(path + "val_pdf_shift")
            val_pdf_sh_exp = CellStatistics([zeros, val_pdf_shift], "SUM", "DATA")
            distr_days = Plus(val_pdf_sh_exp, distr_days)
        if temp_it % 20000 == 0: # Just a print statement to tell me how it's going
                print "Iteration number " + str(temp_it) +" completed at " + str(time_it)
        x += 240 # shift x over one column
        if x > 1538295: # if your at the right hand side of a row
            y = y-240 # Shift y down a row
            x = 959415 # Shift x back to the first left hand column
        temp_it+=1

distr_days.save(path + "Final_distr_days")

4

Tente usar IGridTable, ICursor, IRow. Esse trecho de código é para atualizar os valores das células de varredura, no entanto, mostra o básico da iteração:

Como posso adicionar um novo campo em uma tabela de atributos raster e percorrê-lo?

Public Sub CalculateArea(raster As IRaster, areaField As String)
    Dim bandCol As IRasterBandCollection
    Dim band As IRasterBand

    Set bandCol = raster
    Set band = bandCol.Item(0)

    Dim hasTable As Boolean
    band.hasTable hasTable
    If (hasTable = False) Then
        Exit Sub
    End If    

    If (AddVatField(raster, areaField, esriFieldTypeDouble, 38) = True) Then
        ' calculate cell size
        Dim rstProps As IRasterProps
        Set rstProps = raster

        Dim pnt As IPnt
        Set pnt = rstProps.MeanCellSize

        Dim cellSize As Double
        cellSize = (pnt.X + pnt.Y) / 2#

        ' get fields index
        Dim attTable As ITable
        Set attTable = band.AttributeTable

        Dim idxArea As Long, idxCount As Long
        idxArea = attTable.FindField(areaField)
        idxCount = attTable.FindField("COUNT")

        ' using update cursor
        Dim gridTableOp As IGridTableOp
        Set gridTableOp = New gridTableOp

        Dim cellCount As Long, cellArea As Double

        Dim updateCursor As ICursor, updateRow As IRow
        Set updateCursor = gridTableOp.Update(band.RasterDataset, Nothing, False)
        Set updateRow = updateCursor.NextRow()
        Do Until updateRow Is Nothing
            cellCount = CLng(updateRow.Value(idxCount))
            cellArea = cellCount * (cellSize * cellSize)

            updateRow.Value(idxArea) = cellArea
            updateCursor.updateRow updateRow

            Set updateRow = updateCursor.NextRow()
        Loop

    End If
End Sub

Depois de percorrer a tabela, você pode obter o valor da linha do campo específico usando row.get_Value(yourfieldIndex). Se você Google

arcobjects row.get_Value

você deve conseguir muitos exemplos mostrando isso.

Espero que ajude.


1
Infelizmente, deixei de observar e editarei na minha pergunta original acima acima que minha varredura possui muitos valores contínuos que consistem em grandes valores duplos e, como tal, esse método não funciona porque minha varredura não possui valores de tabela de atributos.
Conor

4

Que tal isso como uma idéia radical, exigiria que você programa em python ou ArcObjects.

  1. Converta sua grade em um recurso de classe de ponto.
  2. Crie campos XY e preencha.
  3. Carregue os pontos em um dicionário onde key é uma sequência de X, Y e item é o valor da célula.
  4. Percorra o seu dicionário e, para cada ponto, elabore os 8 XYs da célula circundante.
  5. Recupere-os do seu dicionário e teste com suas regras, assim que encontrar um valor verdadeiro, você pode pular o restante dos testes.
  6. Escreva os resultados em outro dicionário e depois converta novamente em uma grade criando primeiro um FeatureClass de ponto e depois converta pontos em uma grade.

2
Ao converter para um conjunto de recursos pontuais, essa idéia elimina as duas qualidades da representação de dados baseada em varredura que a tornam tão eficaz: (1) encontrar vizinhos é uma operação em tempo constante extremamente simples e (2) porque o armazenamento explícito de locais é não necessário, os requisitos de RAM, disco e E / S são mínimos. Portanto, embora essa abordagem funcione, é difícil encontrar algum motivo para recomendá-la.
whuber

Obrigado pela sua resposta Hornbydd. Eu estou bem com a implementação de um método como este, mas parece que as etapas 4 e 5 não seriam muito eficazes em termos computacionais. Minhas raspadeiras terão um mínimo de 62.500 células (a resolução mínima para a minha varredura que eu defini é 250 células x 250 células, mas a resolução pode e geralmente consiste em muito mais), e eu teria que fazer uma consulta espacial para cada condição executar minhas comparações ... Como tenho 6 condições, isso seria 6 * 62500 = 375000 consultas espaciais. Eu ficaria melhor com álgebra de mapa. Mas obrigado por esta nova maneira de visualizar o problema. Votado.
Conor

Você não pode simplesmente convertê-lo para ASCII e usar um programa como R para fazer a computação?
Oliver Burdekin

Além disso, eu tenho um applet Java que escrevi que pode ser facilmente modificado para atender às suas condições acima. Era apenas um algoritmo de suavização, mas as atualizações seriam bem fáceis de fazer.
Oliver Burdekin

Desde que o programa possa ser chamado a partir da plataforma .NET para um usuário que possua apenas o .NET Framework 3.5 e o ArcGIS 10 instalados. O programa é de código aberto e pretendo que esses sejam os únicos requisitos de software quando entregues aos usuários finais. Se sua resposta puder ser implementada para atender a esses dois requisitos, ela será considerada uma resposta válida. Também adicionarei uma tag de versão à pergunta para esclarecimentos.
Conor

2

Uma solução:

Eu resolvi isso hoje cedo. O código é uma adaptação desse método . O conceito por trás disso não foi terrivelmente difícil depois que eu descobri o que os objetos usados ​​para interagir com a varredura realmente fazem. O método abaixo utiliza dois conjuntos de dados de entrada (inRasterDS e outRasterDS). Ambos são o mesmo conjunto de dados, eu apenas fiz uma cópia do inRasterDS e a passei para o método como outRasterDS. Dessa forma, ambos têm a mesma extensão, referência espacial, etc. O método lê os valores do inRasterDS, célula por célula, e faz comparações vizinhas mais próximas. Ele usa os resultados dessas comparações como valores armazenados no outRasterDS.

O processo:

Usei o IRasterCursor -> IPixelBlock -> SafeArray para obter os valores de pixel e o IRasterEdit para escrever novos no raster. Ao criar o IPixelBlock, você está informando à máquina o tamanho e o local da área na qual deseja ler / gravar. Se você deseja editar apenas a metade inferior de uma varredura, defina-a como seus parâmetros IPixelBlock. Se você deseja fazer um loop sobre toda a varredura, defina IPixelBlock igual ao tamanho de toda a varredura. Eu faço isso no método abaixo, passando o tamanho para IRasterCursor (pSize) e obtendo o PixelBlock a partir do cursor raster.

A outra chave é que você precisa usar o SafeArray para fazer interface com os valores desse método. Você obtém o IPixelBlock do IRasterCursor e o SafeArray do IPixelBlock. Então você lê e escreve no SafeArray. Quando você terminar de ler / gravar no SafeArray, grave todo o SafeArray de volta no IPixelBlock, depois grave o IPixelBlock no IRasterCursor e, finalmente, use o IRasterCursor para definir o local para iniciar a gravação e o IRasterEdit faça a gravação. Esta etapa final é onde você realmente edita os valores do conjunto de dados.

    public static void CreateBoundaryRaster(IRasterDataset2 inRasterDS, IRasterDataset2 outRasterDS)
    {
        try
        {
            //Create a raster. 
            IRaster2 inRaster = inRasterDS.CreateFullRaster() as IRaster2; //Create dataset from input raster
            IRaster2 outRaster = outRasterDS.CreateFullRaster() as IRaster2; //Create dataset from output raster
            IRasterProps pInRasterProps = (IRasterProps)inRaster;
            //Create a raster cursor with a pixel block size matching the extent of the input raster
            IPnt pSize = new DblPnt();
            pSize.SetCoords(pInRasterProps.Width, pInRasterProps.Height); //Give the size of the raster as a IPnt to pass to IRasterCursor
            IRasterCursor inrasterCursor = inRaster.CreateCursorEx(pSize); //Create IRasterCursor to parse input raster 
            IRasterCursor outRasterCursor = outRaster.CreateCursorEx(pSize); //Create IRasterCursor to parse output raster
            //Declare IRasterEdit, used to write the new values to raster
            IRasterEdit rasterEdit = outRaster as IRasterEdit;
            IRasterBandCollection inbands = inRasterDS as IRasterBandCollection;//set input raster as IRasterBandCollection
            IRasterBandCollection outbands = outRasterDS as IRasterBandCollection;//set output raster as IRasterBandCollection
            IPixelBlock3 inpixelblock3 = null; //declare input raster IPixelBlock
            IPixelBlock3 outpixelblock3 = null; //declare output raster IPixelBlock
            long blockwidth = 0; //store # of columns of raster
            long blockheight = 0; //store # of rows of raster

            //create system array for input/output raster. System array is used to interface with values directly. It is a grid that overlays your IPixelBlock which in turn overlays your raster.
            System.Array inpixels; 
            System.Array outpixels; 
            IPnt tlc = null; //set the top left corner

            // define the 3x3 neighborhood objects
            object center;
            object topleft;
            object topmiddle;
            object topright;
            object middleleft;
            object middleright;
            object bottomleft;
            object bottommiddle;
            object bottomright;

            long bandCount = outbands.Count; //use for multiple bands (only one in this case)

            do
            {

                inpixelblock3 = inrasterCursor.PixelBlock as IPixelBlock3; //get the pixel block from raster cursor
                outpixelblock3 = outRasterCursor.PixelBlock as IPixelBlock3;
                blockwidth = inpixelblock3.Width; //set the # of columns in raster
                blockheight = inpixelblock3.Height; //set the # of rows in raster
                outpixelblock3.Mask(255); //set any NoData values

                for (int k = 0; k < bandCount; k++) //for every band in raster (will always be 1 in this case)
                {
                    //Get the pixel array.
                    inpixels = (System.Array)inpixelblock3.get_PixelData(k); //store the raster values in a System Array to read
                    outpixels = (System.Array)outpixelblock3.get_PixelData(k); //store the raster values in a System Array to write
                    for (long i = 1; i < blockwidth - 1; i++) //for every column (except outside columns)
                    {
                        for (long j = 1; j < blockheight - 1; j++) //for every row (except outside rows)
                        {
                            //Get the pixel values of center cell and  neighboring cells

                            center = inpixels.GetValue(i, j);

                            topleft = inpixels.GetValue(i - 1, j + 1);
                            topmiddle = inpixels.GetValue(i, j + 1);
                            topright = inpixels.GetValue(i + 1, j + 1);
                            middleleft = inpixels.GetValue(i - 1, j);
                            middleright = inpixels.GetValue(i + 1, j);
                            bottomleft = inpixels.GetValue(i - 1, j - 1);
                            bottommiddle = inpixels.GetValue(i, j - 1);
                            bottomright = inpixels.GetValue(i - 1, j - 1);


                            //compare center cell value with middle left cell and middle right cell in a 3x3 grid. If true, give output raster value of 1
                            if ((Convert.ToDouble(center) >= Convert.ToDouble(middleleft)) && (Convert.ToDouble(center) >= Convert.ToDouble(middleright)))
                            {
                                outpixels.SetValue(1, i, j);
                            }


                            //compare center cell value with top middle and bottom middle cell in a 3x3 grid. If true, give output raster value of 1
                            else if ((Convert.ToDouble(center) >= Convert.ToDouble(topmiddle)) && (Convert.ToDouble(center) >= Convert.ToDouble(bottommiddle)))
                            {
                                outpixels.SetValue(1, i, j);
                            }

                            //if neither conditions are true, give raster value of 0
                            else
                            {

                                outpixels.SetValue(0, i, j);
                            }
                        }
                    }
                    //Write the pixel array to the pixel block.
                    outpixelblock3.set_PixelData(k, outpixels);
                }
                //Finally, write the pixel block back to the raster.
                tlc = outRasterCursor.TopLeft;
                rasterEdit.Write(tlc, (IPixelBlock)outpixelblock3);
            }
            while (inrasterCursor.Next() == true && outRasterCursor.Next() == true);
            System.Runtime.InteropServices.Marshal.ReleaseComObject(rasterEdit);


        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }

    }

1

Os dados de varredura do AFAIK podem ser lidos de três maneiras:

  • por célula (ineficiente);
  • por imagem (bastante eficiente);
  • por blocos (a maneira mais eficiente).

Sem reinventar a roda, sugiro ler esses slides esclarecedores de Chris Garrard.

Portanto, o método mais eficiente é ler dados por bloco, no entanto, isso causaria uma perda de dados na correspondência de pixels localizados sobre os limites do bloco durante a aplicação do filtro. Portanto, uma maneira alternativa segura deve consistir na leitura de toda a imagem de uma só vez e na abordagem numpy.

No lado computacional, eu deveria usar o gdalfilter.py e implicitamente a abordagem VRT KernelFilteredSource para aplicar os filtros necessários e, acima de tudo, evitar cálculos pesados.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.