Kameraanwendungen und Bilderkennung werden seit etwa 30 Jahren zur Qualitätskontrolle in der industriellen Produktion eingesetzt. Dabei war stets ein erheblicher Anteil an manueller Merkmalsextraktion erforderlich. Deshalb wurden sie bisher nur für besonders sensible Produktionsschritte eingesetzt. Der Engineering-Aufwand für jede Aufgabe ist individuell und übersteigt bei weitem die Investitionskosten in Hard- und Software. Die Bilderkennung hat gegenüber anderen Sensoren den Vorteil, dass sie für ein breites Spektrum von Szenarien eingesetzt werden kann und berührungslos arbeitet. Computer Vision oder Methoden des maschinellen Sehens auf der Basis von Neuronennetzen bieten neue Möglichkeiten. Einerseits werden Klassifikatoren bereits erfolgreich im industriellen Maßstab eingesetzt, wobei allerdings manuell vorklassifizierte Daten verwendet werden. Die Beschriftung der Daten ist mit erheblichem Aufwand verbunden und die Ergebnisse stellen für den Anwender eine Blackbox dar. Bei Änderungen in der Produktionsumgebung muss der Prozess wiederholt werden.
Seit einiger Zeit werden neuronale Netze erforscht, deren Struktur zu sogenannten Autoencodern führt. Im Grundprinzip werden die neuronalen Schichten zunächst verjüngt und dann wieder erweitert. Anstatt das Label für einen neuen Datensatz korrekt zu bestimmen, werden die Netze darauf trainiert, das am Eingang erzeugte Bild am Ende des Netzes trotz des Engpasses korrekt wiederzugeben. Ist das Training erfolgreich, liegen für jedes Bild im Engpass alle Bildinformationen bzw. Variationsmöglichkeiten in wenigen Zahlenwerten und damit in kodierter oder komprimierter Form vor. Darüber hinaus kann die Qualität durch Reproduktion der Eingabe leicht überprüft werden und Variationen im Engpass geben Aufschluss über das Verhalten des Netzes. Dies wird auch als Unüberwachtes Lernen bezeichnet, während unter Überwachen die Bereitstellung weiterer Informationen über die Daten, wie z.B. beispielhafte Labels, verstanden wird.
Durch Anwendung des trainierten Autoencoders wird nun jedes Bild durch einen Zahlenvektor im Merkmalsraum dargestellt. Mit 2 numerischen Werten ist der Raum 2-dimensional. Jedes Bild entspricht einem Punkt.
Genauer gesagt werden die Freiheitsgrade einer Bildserie im Feature Space dargestellt, während die genaue Bildstruktur statisch in den Gewichten des neuronalen Netzes repräsentiert wird.
Die Freiheitsgrade können nun anstelle eines manuell programmierten Merkmals der klassischen Bilderkennung genutzt werden, z.B. zur Zustandsüberwachung. Der manuelle Engineering-Prozess entfällt. So können Kameras zur Qualitätskontrolle eingesetzt werden, ohne den manuellen Kompromiss zwischen Kosten und Nutzen.
Um dies zu demonstrieren, wurde zunächst ein praxisnaher Versuchsaufbau konzipiert, der einen flexiblen Aufbau beinhaltet und definierte Laborbedingungen bietet. Der Aufbau ist eine miniaturisierte Tür, die stufenlos geöffnet und geschlossen werden kann. Gleichzeitig kann die Beleuchtungsintensität variiert werden, so dass zwei unabhängige externe und zufällig erzeugte Freiheitsgrade eingestellt werden können, die nun vom Auto-Encoder als solche zu erkennen sind.
Abbildung 3 zeigt eine optimierte Konstruktion mit Legosteinen und einer elektronischen Steuerung des Türwinkels und der Beleuchtung, die kompakte Abmessungen von 25x25x25 hat. Die Steuerung des Motors und der Beleuchtung erfolgt über einen Raspberry Pi, das Training und die Echtzeitinformationen über einen Nvidia Jetson Nano, der über eine Webapp ferngesteuert und interaktiv bedient werden kann.
Das Bild der verwendeten Basler Industriekamera wurde auf 200x200 Pixel verkleinert und ein geeignetes Objektiv gewählt, um die Szene ohne Autofokus aufzunehmen.
Ein klassischer Auto-Encoder zeichnet sich durch eine gute Kompressionsfähigkeit und eine gute Rekonstruktionsqualität aus. Er erzeugt jedoch Unstetigkeiten im Merkmalsraum. Dies äußert sich z. B. darin, dass die Türöffnung nicht kontinuierlich abgebildet wird, sondern eine Diskontinuität aufweist und den Merkmalsraum in zwei Segmente aufteilt. Merkmalsvektoren, die nicht genau auf den Kurven liegen, liefern undefinierte und unbrauchbare Rekonstruktionen. Um beliebige Kombinationen von Merkmalsvektoren und damit den gesamten Merkmalsraum nutzen zu können, wurde das Konzept der GAN (Generative Adversarial Networks) eingeführt. Dies ist jedoch für die Zustandsüberwachung nicht geeignet. Für diese spezielle Anwendung liegt der Fokus nicht auf einer qualitativ hochwertigen Rekonstruktion und beliebigen Kombination von Merkmalen, sondern auf einem möglichst kompakten Merkmalsraum, der zwar Unstetigkeiten vermeidet, aber nicht jede Kombination abbilden soll. Für diesen Kompromiss hat sich ein klassischer Autoencoder bewährt, der um ein zweites Diskriminatornetz erweitert wird, das für eine definierte Verteilung des Merkmalsraums sorgt und damit die Kontinuität gewährleistet. Man könnte sagen, dass dieser Adversarial Autoencoder die Vorteile beider Ansätze vereint. Im Folgenden wird der Code zum Aufbau des Netzes in Keras gezeigt, einem Framework zur Modellierung neuronaler Netze, das die Verwendung verschiedener Frameworks im Backend erlaubt. Hier wurden alternativ sowohl Tensorflow als auch plaidML verwendet, wobei keine großen Unterschiede festgestellt werden konnten:
classAAE():
\# -----------------------------------------------------------------------------
\# Build the adversarial autoencoder and load the features from the last
\# training session.
def\_\_init\_\_(self,epochs=5,batchSize=32):
self.imageShape=(200,200,3)
self.latentDim=2
self.channels=3
self.epochs=epochs
self.batchSize=batchSize
self.latentPoints=\[]
optimizer=Adam(0.0002,0.5)
metric='accuracy'
binaryLoss='binary_crossentropy'
\# build discriminator
self.discriminator=self.buildDiscriminator()
self.discriminator.compile(optimizer=optimizer,loss=binaryLoss,
metrics=\[metric])
\# build encoder and decoder
self.encoder=self.buildEncoder()
self.decoder=self.buildDecoder()
\# define picture input
image=Input(shape=self.imageShape)
\# encoderResult are the features extracted
\# from the the images by the encoder
encoderResult=self.encoder(image)
\# decoderResult are the images reconstructed by the decoder
\# from the encoderResult
decoderResult=self.decoder(encoderResult)
\# set discriminator to untrainable
forlayerinself.discriminator.layers:
layer.trainable=False
\# discriminatorResult is the evaluation of the encoderResult
\# by the discriminator
discriminatorResult=self.discriminator(encoderResult)
\# create the autoencoder Model by assigning images as input and
\# reconstructed images (decoderResult) & discriminatorResult as outputs
self.autoencoder=Model(image, \[decoderResult, discriminatorResult])
\# compiling autoencoder, setting lossfuntion for the difference
\# between input image and reconstructed image to MSE &
\# lossfunction of discriminator result to Binary Crossentropy
self.autoencoder.compile(optimizer=optimizer,
loss=\['mse','binary_crossentropy'],
loss_weights=\[0.999,0.001])
self.loadLatentPoints()
\# -----------------------------------------------------------------------------
\# Build the encoder and return the Model
defbuildEncoder(self):
encoder=Sequential()
encoder.add(Conv2D(32,kernel_size=5,
input_shape=self.imageShape,padding='same',
kernel_initializer='random_uniform',
bias_initializer='zeros'))
encoder.add(LeakyReLU(alpha=0.2))
encoder.add(MaxPooling2D(pool_size=(2,2)))
encoder.add(Conv2D(64,kernel_size=5,padding='same',
kernel_initializer='random_uniform',
bias_initializer='zeros'))
\# encoder.add(BatchNormalization(axis=1))
encoder.add(LeakyReLU(alpha=0.2))
encoder.add(MaxPooling2D(pool_size=(5,5)))
encoder.add(Conv2D(128,kernel_size=5,padding='same',
kernel_initializer='random_uniform',
bias_initializer='zeros'))
\# encoder.add(BatchNormalization(axis=1))
encoder.add(LeakyReLU(alpha=0.2))
encoder.add(MaxPooling2D(pool_size=(5,5)))
encoder.add(Flatten())
encoder.add(Dense(self.latentDim,kernel_initializer='random_uniform',
bias_initializer='zeros'))
encoder.summary()
image=Input(shape=(self.imageShape))
latentCode=encoder(image)
returnModel(image, latentCode)
\# -----------------------------------------------------------------------------
\# Build the decoder and return the Model
defbuildDecoder(self):
decoder=Sequential()
decoder.add(Dense(128\*4\*4,activation="relu",
input_dim=self.latentDim,
kernel_initializer='random_uniform',
bias_initializer='zeros'))
decoder.add(Reshape((4,4,128)))
\# decoder.add(BatchNormalization())
decoder.add(LeakyReLU(alpha=0.2))
decoder.add(Conv2DTranspose(64,kernel_size=5,strides=5,
padding="same",
kernel_initializer='random_uniform',
bias_initializer='zeros'))
\# decoder.add(BatchNormalization())
decoder.add(LeakyReLU(alpha=0.2))
decoder.add(Conv2DTranspose(32,kernel_size=5,strides=5,
padding="same",
kernel_initializer='random_uniform',
bias_initializer='zeros'))
\# decoder.add(BatchNormalization())
decoder.add(LeakyReLU(alpha=0.2))
decoder.add(Conv2DTranspose(self.channels,kernel_size=5,strides=2,
padding="same",
kernel_initializer='random_uniform',
bias_initializer='zeros'))
\# decoder.add(BatchNormalization())
decoder.add(LeakyReLU(alpha=0.2))
decoder.add(Activation("tanh"))
decoder.summary()
z=Input(shape=(self.latentDim,))
reconstructionResult=decoder(z)
returnModel(z, reconstructionResult)
\# -----------------------------------------------------------------------------
\# Build the discriminator and return the Model
defbuildDiscriminator(self):
discriminator=Sequential()
discriminator.add(Dense(300,activation='relu',
input_dim=self.latentDim,
kernel_initializer='random_uniform',
bias_initializer='zeros'))
discriminator.add(Dense(300,activation='relu',
kernel_initializer='random_uniform',
bias_initializer='zeros'))
discriminator.add(Dense(1,activation='sigmoid',
kernel_initializer='random_uniform',
bias_initializer='zeros'))
discriminator.summary()
encoderResult=Input(shape=(self.latentDim,))
discriminatorResult=discriminator(encoderResult)
returnModel(encoderResult, discriminatorResult)
\# -----------------------------------------------------------------------------
\# Train the adversarial autoencoder model, save the features & model
\#
\# INPUT: - "filepaths" path to the folder containing the training data
\# relative to the folder this python code is
\# located in.
deftrain(self,filepaths):
forepochinrange(self.epochs):
random.shuffle(filepaths)
batches=len(filepaths)//self.batchSize
valid=np.ones((self.batchSize,1))
fake=np.zeros((self.batchSize,1))
foriinrange(batches):
print('epoch:{}batch:{}/{}'.format(epoch+1, i+1,
batches))
images=self.loadImages(
filepaths\[i\*self.batchSize:(i+1)\*self.batchSize])
latent_fake=self.encoder.predict(images)
latent_real=np.random.uniform(-2,2,size=(self.batchSize,
self.latentDim))
discriminator_loss_real=self.discriminator.train_on_batch(
latent_real, valid)
discriminator_loss_fake=self.discriminator.train_on_batch(
latent_fake, fake)
discriminator_loss=0.5*np.add(
discriminator_loss_real, discriminator_loss_fake)
genLoss=self.autoencoder.train_on_batch(
images, \[images, valid])
print(discriminator_loss\[0], genLoss\[0])
self.drawLatentPoints(filepaths)
self.saveModel()
\# -----------------------------------------------------------------------------
\# Additiuonal helper functions as well as the whole code for the backend
\# can be found in the git repo
\# -----------------------------------------------------------------------------
Das Frontend wurde so gestaltet, dass verschiedene Bilder im Backend angezeigt und mit einem vorkonfigurierten Netzwerk auf Knopfdruck trainiert werden können. Die Merkmale wurden in einem Diagramm in Echtzeit abgebildet und eine rechteckige Fensterauswahl ermöglicht eine rudimentäre Zustandsüberwachung der Merkmale.