giovedì 15 febbraio 2018

Esperimenti con filtri Canny ed Hough Lines con OpenCV







----------------------------------------------------------------------------------------------





----------------------------------------------------------------------------------------------
import cv2
import numpy as np
from matplotlib import pyplot as plt

img = cv2.imread('casa.jpg')
gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

# parametri automatici Canny
#ret,th = cv2.threshold(gray,0,255,cv2.THRESH_BINARY+cv2.THRESH_OTSU)
#edges = cv2.Canny(img,ret,ret*0.5)

v = np.median(gray)
sigma = 0.3
lower = int(max(0,(1.0-sigma)*v))
upper = int(max(0,(1.0+sigma)*v))

print lower
print upper

edges = cv2.Canny(img,lower,upper)

cv2.imwrite('canny_casa.jpg',edges)


lines = cv2.HoughLines(edges,1.5,np.pi/180,370)
for rho,theta in lines[0]:
    a = np.cos(theta)
    b = np.sin(theta)
    x0 = a*rho
    y0 = b*rho
    x1 = int(x0 + 1000*(-b))
    y1 = int(y0 + 1000*(a))
    x2 = int(x0 - 1000*(-b))
    y2 = int(y0 - 1000*(a))

    cv2.line(img,(x1,y1),(x2,y2),(0,0,255),2)

cv2.imwrite('risultato_casa.jpg',img)



martedì 13 febbraio 2018

Dual boot Ubuntu su MacBook

Avevo gia' messo in dual boot MacOs X con Ubuntu su un MacBook qualche anno fa ma non mi ero appuntato la procedura e cosi' ho dovuto reimparare da capo..questa volta scrivo qui i passi.

Il Mac parte tranquillamente da una normale Ubuntu Live 16.04.03 mettendo la penna USB e premendo il tasto Option al boot.


Invece di fare partire l'installer ho lanciato GParted (che e' compreso nella live) per ridurre la dimensione della partizione OSX e lasciare posto a Ubuntu creando le due partizioni di root e di swap. A questo punto si lancia l'installer e fa tutto in automatico...anche troppo...infatti quando ho visto che installava Grub senza chiedermi niente ho capito di essere nei guai.
Al riavvio successivo infatti e' partita in automatico Ubuntu senza possibilita' di scegliere Linux...attimo di panico e riavvio premendo il tasto Option ha fatto vedere la partizione MacOsx

E' arrivato il momento di aggiungere rEFInd, il boot manager, ma prima dovevo disabilitare SIP (crutil disable), operazione di effettua dalla modalita' Recovery di Mac....tranne per il fatto che il computer non ne voleva sapere di Command+R. Ho scoperto dopo un po' di prove che priva dovevo selezionare la partizione Mac con Option e poi dare Command+R per entrare in Recovery



Fatto cio' ed montato rEFInd l'ultimo problema e' stato quello di configurare la scheda WiFi di Ubuntu con

sudo apt-get update
sudo apt-get install firmware-b43-installer
mancherebbe da settare la retroilluminazione della tastiera ma per questo dettaglio c'e' tempo

lunedì 12 febbraio 2018

Realtime Hough Lines con OpenCV su Android

L'idea e' quella di utilizzare OpenCV per estrarre le linee in realtime dalle immagini riprese dalla fotocamera del telefono



Dopo avere impostato un nuovo progetto OpenCV su Android come visto qui ho modificato il secondo esempo (mixed_sample)

Un po' di commenti
1) il flusso di lavoro prevede di prendere l'immagine dalla fotocamera, convertirla in scala di grigi, applicare il filtro Canny, applicare l'algoritmo delle Hough Lines,  selezionare solo le linee maggiore di un valore limite (altrimenti l'algoritmo diventa rumoroso), calcolare il coefficiente angolare di ogni linea per creare 5 classi angolari (-90°/-60°,-60°/-30°,-30°/0°,0°/30°,30°/60°,60°/90°),  popolare una textbox in altro a sinistra con i valori delle classi,sovrapporre le Hough Lines all'immagine RGB (Img.procline) e mostrare il tutto sullo schermo

2) per poter modificare la textbox si deve procedere mediante RunonUIThread




uno dei problemini che si hanno usando i filtri con i parametri cosi' come sono impostati e' che
in scene reali complesse, il filtro canny puo' comportarsi in questo modo


rendendo totalmente inutile la ricerca delle houghlines. Per risolvere questo problema possono essere calcolati i valori di threshold in modo automatico dalla deviazione media dell'immagine come segue

MatOfDouble mu = new MatOfDouble();
MatOfDouble sigma = new MatOfDouble();

Core.meanStdDev(inputFrame.gray(),mu,sigma);

Mat lines = new Mat();
Imgproc.Canny(inputFrame.gray(), mIntermediateMat, mu.get(0,0)[0] - sigma.get(0,0)[0], mu.get(0,0)[0] - sigma.get(0,0)[0]); 

per fare le cose in modo piu' pulito possono essere usate le funzioni di OpenCV come il filtro adattativo

Imgproc.adaptiveThreshold(inputFrame.gray(),result,255,
Imgproc.ADAPTIVE_THRESH_GAUSSIAN_C,Imgproc.THRESH_OTSU,15,4)


Layout
---------------------------------
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"    
xmlns:tools="http://schemas.android.com/tools"   
 xmlns:opencv="http://schemas.android.com/apk/res-auto"  
  android:layout_width="match_parent"    
android:layout_height="match_parent" >

    <org.opencv.android.JavaCameraView        
android:layout_width="fill_parent"        
android:layout_height="fill_parent"        
android:visibility="gone"        
android:id="@+id/tutorial1_activity_java_surface_view"        
opencv:show_fps="true"        
opencv:camera_id="any" />

    <LinearLayout        
android:layout_width="match_parent"        
android:layout_height="match_parent"        
android:orientation = "vertical" >

        <TextView            
android:layout_width="wrap_content"            
android:layout_height="wrap_content"            
android:id="@+id/fps_text_view"            
android:textSize="12sp"
            android:text="FPS:" />

    </LinearLayout>



</FrameLayout>
---------------------------------
Codice
---------------------------------

import android.app.Activity;
import android.graphics.Color;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.SurfaceView;
import android.view.WindowManager;
import android.widget.TextView;

import org.opencv.android.OpenCVLoader;

import org.opencv.android.BaseLoaderCallback;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewFrame;
import org.opencv.android.LoaderCallbackInterface;
import org.opencv.core.CvType;
import org.opencv.core.Mat;
import org.opencv.android.CameraBridgeViewBase;
import org.opencv.android.CameraBridgeViewBase.CvCameraViewListener2;
import org.opencv.core.Point;
import org.opencv.core.Scalar;
import org.opencv.imgproc.Imgproc;

import java.util.function.DoubleToLongFunction;

public class MainActivity extends Activity implements CvCameraViewListener2 {

    private static final String    TAG = "OCVSample::Activity";

    private static final int       VIEW_MODE_RGBA     = 0;
    private static final int       VIEW_MODE_GRAY     = 1;
    private static final int       VIEW_MODE_CANNY    = 2;
    private static final int       VIEW_MODE_FEATURES = 5;
    private static final int       VIEW_MODE_HOUGH = 3;


    private int                    mViewMode;
    private Mat                    mRgba;
    private Mat                    mIntermediateMat;
    private Mat                    mGray;

    private MenuItem               mItemPreviewRGBA;
    private MenuItem               mItemPreviewGray;
    private MenuItem               mItemPreviewCanny;
    private MenuItem               mItemPreviewFeatures;

    private CameraBridgeViewBase   mOpenCvCameraView;

    int [] classi = new int[10];


    int threshold = 50;
    int minLineSize = 20;
    int lineGap = 10;


    private BaseLoaderCallback  mLoaderCallback = new BaseLoaderCallback(this) {
        @Override        public void onManagerConnected(int status) {
            switch (status) {
                case LoaderCallbackInterface.SUCCESS:
                {
                    Log.i(TAG, "OpenCV loaded successfully");

                    mOpenCvCameraView.enableView();
                } break;
                default:
                {
                    super.onManagerConnected(status);
                } break;
            }
        }
    };
    private TextView testo;


    /** Called when the activity is first created. */    @Override    public void onCreate(Bundle savedInstanceState) {
        Log.i(TAG, "called onCreate");
        super.onCreate(savedInstanceState);
        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);

        setContentView(R.layout.activity_main);

        testo = (TextView) findViewById(R.id.fps_text_view);
        testo.setTextColor(Color.RED);


        mOpenCvCameraView = (CameraBridgeViewBase) findViewById(R.id.tutorial1_activity_java_surface_view);
        mOpenCvCameraView.setVisibility(CameraBridgeViewBase.VISIBLE);
        mOpenCvCameraView.setCvCameraViewListener(this);
        mViewMode = VIEW_MODE_HOUGH;
    }

    @Override    public boolean onCreateOptionsMenu(Menu menu) {
        Log.i(TAG, "called onCreateOptionsMenu");
        mItemPreviewRGBA = menu.add("Preview RGBA");
        mItemPreviewGray = menu.add("Preview GRAY");
        mItemPreviewCanny = menu.add("Canny");
        mItemPreviewFeatures = menu.add("Find features");
        return true;
    }

    @Override    public void onPause()
    {
        super.onPause();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    @Override    public void onResume()
    {
        super.onResume();
        if (!OpenCVLoader.initDebug()) {
            Log.d(TAG, "Internal OpenCV library not found. Using OpenCV Manager for initialization");
            OpenCVLoader.initAsync(OpenCVLoader.OPENCV_VERSION_3_0_0, this, mLoaderCallback);
        } else {
            Log.d(TAG, "OpenCV library found inside package. Using it!");
            mLoaderCallback.onManagerConnected(LoaderCallbackInterface.SUCCESS);
        }
    }

    public void onDestroy() {
        super.onDestroy();
        if (mOpenCvCameraView != null)
            mOpenCvCameraView.disableView();
    }

    public void onCameraViewStarted(int width, int height) {
        mRgba = new Mat(height, width, CvType.CV_8UC4);
        mIntermediateMat = new Mat(height, width, CvType.CV_8UC4);
        mGray = new Mat(height, width, CvType.CV_8UC1);
    }

    public void onCameraViewStopped() {
        mRgba.release();
        mGray.release();
        mIntermediateMat.release();
    }

    public static boolean isBetween(double x, double lower, double upper) {
        return lower <= x && x <= upper;
    }


    public Mat onCameraFrame(CvCameraViewFrame inputFrame) {
        final int viewMode = mViewMode;
        switch (viewMode) {
            case VIEW_MODE_GRAY:
                // input frame has gray scale format                
               Imgproc.cvtColor(inputFrame.gray(), mRgba, Imgproc.COLOR_GRAY2RGBA, 4);
                break;
            case VIEW_MODE_RGBA:
                // input frame has RBGA format                
               mRgba = inputFrame.rgba();
                break;
            case VIEW_MODE_CANNY:
                // input frame has gray scale format                mRgba = inputFrame.rgba();
                Imgproc.Canny(inputFrame.gray(), mIntermediateMat, 80, 100);
                Imgproc.cvtColor(mIntermediateMat, mRgba, Imgproc.COLOR_GRAY2RGBA, 4);


                break;
            case VIEW_MODE_HOUGH:

                mRgba = inputFrame.rgba();

                Mat lines = new Mat();
                Imgproc.Canny(inputFrame.gray(), mIntermediateMat, 80, 100);
                Imgproc.HoughLinesP(mIntermediateMat,lines, 1, Math.PI/180, threshold,minLineSize, lineGap);


                Log.d("Linee", Integer.toString(lines.rows()));



                for (int x = 0; x < lines.rows(); x++) {
                    double[] vec = lines.get(x, 0);
                    double x1 = vec[0],
                            y1 = vec[1],
                            x2 = vec[2],
                            y2 = vec[3];
                    Point start = new Point(x1, y1);
                    Point end = new Point(x2, y2);
                    double dx = x1 - x2;
                    double dy = y1 - y2;

                    double m = Math.toDegrees(Math.atan(dy / dx));
                    Log.d("Angolo", Double.toString(m));

                    double dist = Math.sqrt(dx * dx + dy * dy);
                    Log.d("Distanza", Double.toString(dist));


                    //calcola le classi in base all'angolo del coefficiente angolare
                    if (dist > 50.d)                    {
                                if (isBetween(m, -90.0, -60.0)) {
                                    classi[0] = classi[0] + 1;
                                }

                                if (isBetween(m, -60.0, -30.0)) {
                                    classi[1] = classi[1] + 1;
                                }


                                if (isBetween(m, -30.0, 0.0)) {
                                    classi[2] = classi[2] + 1;
                                }

                                if (isBetween(m, 0.0, 30.0)) {
                                    classi[3] = classi[3] + 1;
                                }

                                if (isBetween(m, 30.0, 60.0)) {
                                    classi[4] = classi[4] + 1;
                                }

                                if (isBetween(m, 60.0, 90.0)) {
                                    classi[5] = classi[5] + 1;
                                }
                        //sovrappone le Hough Lines
                        Imgproc.line(mRgba, start, end, new Scalar(255,0, 0, 1),2);
                }


                 

                }



                break;
            case VIEW_MODE_FEATURES:
                // input frame has RGBA format                mRgba = inputFrame.rgba();
                mGray = inputFrame.gray();
                break;
        }


        runOnUiThread(new Runnable() {
            @Override            public void run() {
                testo.setText(" -90/-60 "+Integer.toString(classi[0])+"\n" +
                              " -60/-30 "+Integer.toString(classi[1])+"\n" +
                              " -30/0   "+Integer.toString(classi[2])+"\n" +
                              "   0/30  "+Integer.toString(classi[3])+"\n" +
                              "  30/60  "+Integer.toString(classi[4])+"\n" +
                              "  60/90  "+Integer.toString(classi[5])+"\n"
                );
            }
        });

        return mRgba;
    }

    public boolean onOptionsItemSelected(MenuItem item) {
        Log.i(TAG, "called onOptionsItemSelected; selected item: " + item);

        if (item == mItemPreviewRGBA) {
            mViewMode = VIEW_MODE_RGBA;
        } else if (item == mItemPreviewGray) {
            mViewMode = VIEW_MODE_GRAY;
        } else if (item == mItemPreviewCanny) {
            mViewMode = VIEW_MODE_CANNY;
        } else if (item == mItemPreviewFeatures) {
            mViewMode = VIEW_MODE_FEATURES;
        }

        return true;
    }


}
---------------------------------

Opencv Android SDK su Android Studio

per utilizzare OpenCV su Android mediante l'SDK si procede prima scaricando il voluminoso pacchetto (sono oltre 300 Mb ma sono contenuti gli apk di tutte le applicazioni demo, la documentazione e le librerie compilate per differenti piattaforme hardware)

A questo punto da Android Studio (devono esserer gia' installati NDK e CMake) si crea un normale progetto ... poi /File/New/Import Module e si cerca la directory nell'SDK opencv/sdk/java

Si deve poi editare il file opencv/build.gradle

CompileSDKVersion 23
buildToolsVersion "23.0.2"
TargetSDKVersion 23

fatto cio' si mette la visualizzazione dell'albero di destra (in Project Files) si seleziona Android/App/tasto destro/Open Module Settings/Dependecies/tasto +/Module Dependencies/ e si seleziona OpenCV Library


si va poi in App/New/Folder/JNIFolder/Change Folder Location e si seleziona /src/main/jniLibs

si apre il folder e si copiano tutte le sottodirectories dall'SDK opncv/sdk/native/libs/ cancellando poi cio' che non e' *.so

Per provare se se le cose funzionavano ho provato ad usare il secondo esempio dell'SDK (quello che implementa il filtro Canny in realtime) ed il programma di esempio si ostinava a non funzionare. Dopo un po' di tempo perso a pigiare tasti ho scoperto che commentando la riga

System.loadLibrary("mixed_sample")

tutto funziona correttamente


giovedì 8 febbraio 2018

Ricerca di tweet

Un semplice esempio di come ricercare in modo automatico dei tweet mediante l'uso delle API di Twitter. E' stata impiegata la libreria Python TwitterSearch modificando leggermente il file di esempio per cerca i tweet con hashtag Frana nel dintorni di 100 Km intorno alla coordinata lat 43, lng 11

Per ottenere le chiavi e gli access token si va all'indirizzo https://apps.twitter.com/ e si crea una nuova app

--------------------------------
from TwitterSearch import *

try:
tso = TwitterSearchOrder()
tso.set_keywords(['#Frana'])
tso.set_language('it')
tso.set_geocode(43.0,11.0,100)
tso.set_include_entities(False)

ts = TwitterSearch(
consumer_key = 'xxxxx',
consumer_secret = 'xxxx',
access_token = 'xxxx',
access_token_secret = 'xxxx'
)

for tweet in ts.search_tweets_iterable(tso):
print tweet['user']['screen_name']
print tweet['text']
print tweet['coordinates']
print tweet['created_at']
print "--------------------------"
except TwitterSearchException as e:
print(e)

--------------------------------

Ho notato che ci possono essere problemi con i caratteri Unicode (per esempio se si scaricano tweet in giapponese)

Questo il risultato della query del programma precedente (ricerca dell'hashtag Frana nei dintorni di Firenze



allarme24
RT @muoversintoscan: #SP477 dell'Alpe di Casaglia, avviata la rimozione di massi e detriti precipitati per la #frana. Al più presto una cor…
None
Tue Feb 06 10:33:03 +0000 2018
--------------------------
CittaMetro_FI
RT @muoversintoscan: #SP477 dell'Alpe di Casaglia, avviata la rimozione di massi e detriti precipitati per la #frana. Al più presto una cor…
None
Tue Feb 06 10:31:11 +0000 2018
--------------------------
FlorenceTV_
RT @muoversintoscan: #SP477 dell'Alpe di Casaglia, avviata la rimozione di massi e detriti precipitati per la #frana. Al più presto una cor…
None
Tue Feb 06 10:14:09 +0000 2018
--------------------------
muoversintoscan
#SP477 dell'Alpe di Casaglia, avviata la rimozione di massi e detriti precipitati per la #frana. Al più presto una… https://t.co/nCSHFc57yw
None
Tue Feb 06 10:13:30 +0000 2018
--------------------------
LuceverdeRadio
RT @muoversintoscan: ‼️ #Londa #frana ► operai sul posto per ripristino frana sulla Sp556. Fino alle 18 aperta via di Campobello con access…
None
Mon Feb 05 16:55:55 +0000 2018
--------------------------
muoversintoscan
‼️ #Londa #frana ► operai sul posto per ripristino frana sulla Sp556. Fino alle 18 aperta via di Campobello con acc… https://t.co/fNj0eGdlCt
None
Mon Feb 05 16:43:48 +0000 2018
--------------------------
CittaMetro_FI
RT @FlorenceTV_: #Frana sulla #SP556 #Stia-#Londa, intervento @CittaMetro_FI per riaprire il tratto interessato
#viabiliTOS
https://t.co/EB…
None
Mon Feb 05 16:38:50 +0000 2018
--------------------------
FlorenceTV_
#Frana sulla #SP556 #Stia-#Londa, intervento @CittaMetro_FI per riaprire il tratto interessato
#viabiliTOS… https://t.co/rppYdf3Gpg
None
Mon Feb 05 16:33:30 +0000 2018
--------------------------
muoversintoscan
🚧 In corso intervento @CittaMetro_FI per riaprire a senso unico alt. della #SP556 #Londa-#Stia al km 3+800 interess… https://t.co/2cT6vwwDPQ
None
Mon Feb 05 16:02:25 +0000 2018
--------------------------
muoversintoscan
🚫 Sulla #SP477 dell'Alpe di Casaglia interruzione della circolazione per #frana al km 15+900 dal #2febbraio al term… https://t.co/EppWeU7bme
None
Fri Feb 02 16:31:10 +0000 2018
--------------------------
TgrRai
RT @TgrRaiToscana: Tra poco nel tg il caso della villa di #Ripafratta sepolta da una #frana: nessun lavoro, nè risarcimento. Posta una targ…
None
Tue Jan 30 12:36:26 +0000 2018
--------------------------
TgrRaiToscana
Tra poco nel tg il caso della villa di #Ripafratta sepolta da una #frana: nessun lavoro, nè risarcimento. Posta una… https://t.co/e6Ww36ctov
None
Tue Jan 30 12:10:41 +0000 2018

Windows 7 in Virtualbox

Tentando di montare una macchina virtuale Windows 7 64 bit su VirtualBox (con host Mac) sono incappato in questo errore nelle primissime fasi

Error code : 0x000000C4



Il classico in cui se non ci fossero i forum non ne sarei mai venuto a capo. La soluzione e' quella di inserire la stringa da linea di comando prima di avviare la macchina virtuale



VBoxManage setextradata "Windows 7" VBoxInternal/CPUM/CMPXCHG16B 1

Fatto cio' l'installazione procede senza problemi

mercoledì 31 gennaio 2018

Esempio mini blockchain

Uno stupido esempio di blockchain senza peer to peer e senza POW...solo per fare vedere come sono collegati i vari record e come si valida un blocco


------------------------------------------------------------------------
import sqlite3, hashlib, time

conn = sqlite3.connect("blockchain.txt")
c =conn.cursor()
c.execute("CREATE TABLE IF NOT EXISTS blocks (id integer PRIMARY KEY AUTOINCREMENT, tempo integer NOT NULL,data text NOT NULL, hash text NOT NULL, precedentehash text NOT NULL)")
c.execute("DELETE FROM blocks")

#INSERISCE IL PRIMO RECORD 
#il precedenteh puo' essere impostato come si desidera
indice = 0
precedentehash = "0"
millis = int(round(time.time() * 1000))
data = "Luca"
hash = hashlib.sha256(str(indice) + precedentehash + str(millis) + data)
hex_dig = hash.hexdigest()

query = "INSERT INTO blocks (tempo,data,hash,precedentehash) VALUES ("+str(millis)+",'"+data+"','" + hex_dig+"','" + precedentehash + "')"

print query

c.execute(query)
conn.commit()

# SECONDO RECORD

c.execute("SELECT * FROM blocks ORDER BY id DESC LIMIT 1")
row =  c.fetchone()
precedentehash = row[3]
indice = row[0]+1
millis = int(round(time.time() * 1000))
data ="Innocenti"
hash = hashlib.sha256(str(indice) + precedentehash + str(millis) + data)
hex_dig = hash.hexdigest()
query = "INSERT INTO blocks (tempo,data,hash,precedentehash) VALUES ("+str(millis)+",'"+data+"','" + hex_dig+"','" + precedentehash + "')"
print query

c.execute(query)
conn.commit()


# TERZO RECORD

c.execute("SELECT * FROM blocks ORDER BY id DESC LIMIT 1")
row =  c.fetchone()
precedentehash = row[3]
indice = row[0]+1

millis = int(round(time.time() * 1000))
data ="Firenze"
hash = hashlib.sha256(str(indice) + precedentehash + str(millis) + data)
hex_dig = hash.hexdigest()
query = "INSERT INTO blocks (tempo,data,hash,precedentehash) VALUES ("+str(millis)+",'"+data+"','" + hex_dig+"','" + precedentehash + "')"
print query

c.execute(query)
conn.commit()

#un blocco e' valido se
# l'indice e' maggiore del blocco precedente
# il precedenteh del blocco in esame e' uguale all'hash del record precedente
# l'hash del blocco e' conforme
print "---------------------------------------------"
#controlla l'integrita' del blocco 3
c.execute("SELECT * FROM blocks WHERE id=3")
c3 = c.fetchone()
c.execute("SELECT * FROM blocks WHERE id=2")
c2 = c.fetchone()

if c2[0] ==(c3[0]-1):
    print "Indice OK"
if c2[3] == c3[4]:
    print "HASH precedente Corretto"
    
precedentehash = c3[4]
millis = c3[1]
data = c3[2]
indice = c3[0]
hash = hashlib.sha256(str(indice) + precedentehash + str(millis) + data)
hex_dig = hash.hexdigest()

if c3[3] == hex_dig:
    print "Blocco corretto"

   
print "---------------------------------------------"

# mostra tutto il DB
c.execute("SELECT * FROM blocks")
rows = c.fetchall()
for r in rows:
    print(r)

conn.close()

Debugger integrato ESP32S3

Aggiornamento In realta' il Jtag USB funziona anche sui moduli cinesi Il problema risiede  nell'ID USB della porta Jtag. Nel modulo...